Skip to content
Permalink
Browse files

Merge branch 'saas-12.2-mrp_reservation-arm' into master-byproduct-arm

  • Loading branch information...
amoyaux committed Mar 18, 2019
2 parents 43c681a + 7fbf959 commit 1f09e189062e0bbc9501d104a95036e6dbbd3358
Showing with 166 additions and 35 deletions.
  1. +36 −29 addons/mrp/models/mrp_abstract_workorder.py
  2. +125 −3 addons/mrp/tests/test_order.py
  3. +5 −3 addons/stock/models/stock_move.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import defaultdict

from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
@@ -71,7 +73,7 @@ def _update_workorder_lines(self):
# Remove or lower quantity on exisiting workorder lines
if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
qty_todo = abs(qty_todo)
for workorder_line in move_workorder_lines:
for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl.qty_reserved):
if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0:
break
if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0:
@@ -83,27 +85,39 @@ def _update_workorder_lines(self):
line_values['to_delete'] = workorder_line
else:
new_val = workorder_line.qty_to_consume - qty_todo
new_reserved = max(0, workorder_line.qty_reserved - qty_todo)
line_values['to_update'][workorder_line] = {
'qty_to_consume': new_val,
'qty_done': new_val,
'qty_reserved': new_val,
'qty_reserved': new_reserved,
}
qty_todo = 0
else:
# Search among wo lines which one could be updated
for move_line in move.move_line_ids:
# Get workorder lines that match reservation.
candidates = move_workorder_lines._find_candidate(move_line)
for candidate in candidates:
if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0:
break
qty_to_add = move_line.product_uom_qty - candidate.qty_done
line_values['to_update'][candidate] = {
'qty_done': candidate.qty_done + qty_to_add,
'qty_to_consume': candidate.qty_to_consume + qty_to_add,
'qty_reserved': candidate.qty_reserved + qty_to_add,
qty_reserved_wl = defaultdict(float)
for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl.qty_reserved, reverse=True):
rounding = workorder_line.product_uom_id.rounding
if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0:
break
move_lines = workorder_line._get_move_lines()
qty_reserved_wl[workorder_line.lot_id] += workorder_line.qty_reserved
qty_reserved_remaining = sum(move_lines.mapped('product_uom_qty')) - sum(move_lines.mapped('qty_done')) - qty_reserved_wl[workorder_line.lot_id]
if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0:
qty_to_add = min(qty_reserved_remaining, qty_todo)
line_values['to_update'][workorder_line] = {
'qty_done': workorder_line.qty_done + qty_to_add,
'qty_to_consume': workorder_line.qty_to_consume + qty_to_add,
'qty_reserved': workorder_line.qty_reserved + qty_to_add,
}
qty_todo -= qty_to_add
qty_reserved_wl[workorder_line.lot_id] += qty_to_add

if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial':
line_values['to_update'][workorder_line] = {
'qty_done': workorder_line.qty_done + qty_todo,
'qty_to_consume': workorder_line.qty_to_consume + qty_todo,
}
qty_todo = 0

# if there are still qty_todo, create new wo lines
if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0:
@@ -132,18 +146,18 @@ def _generate_lines_values(self, move, qty_to_consume):
line.lot_id == move_line.lot_id
)
if linked_wo_line:
if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty, precision_rounding=move.product_uom.rounding) < 0:
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - sum(linked_wo_line.mapped('qty_to_consume')))
if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0:
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume')))
else:
continue
else:
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty)
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done)
line = {
'move_id': move.id,
'product_id': move.product_id.id,
'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id,
'qty_to_consume': to_consume_in_line,
'qty_reserved': move_line.product_uom_qty,
'qty_reserved': to_consume_in_line,
'lot_id': move_line.lot_id.id,
'qty_done': to_consume_in_line,
'is_produced': move in self.production_id.move_finished_ids,
@@ -209,7 +223,7 @@ def _update_finished_move(self):
else:
rounding = production_move.product_uom.rounding
production_move._set_quantity_done(
production_move.quantity_done + float_round(self.qty_producing, precision_rounding=rounding)
float_round(self.qty_producing, precision_rounding=rounding)
)

def _update_raw_moves(self):
@@ -322,7 +336,7 @@ def _create_extra_move_lines(self):
# quantity into stock, we take the move location. Anyway, no
# reservation is made, so it is still possible to change it afterwards.
for quant in quants:
quantity = quant.reserved_quantity - quant.quantity
quantity = quant.quantity - quant.reserved_quantity
rounding = quant.product_uom_id.rounding
if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or
float_compare(quantity, 0, precision_rounding=rounding) <= 0):
@@ -364,16 +378,9 @@ def _create_extra_move_lines(self):

return vals_list

def _find_candidate(self, move_line):
""" Method used in order to return move lines that match reservation.
The purpose is to update exisiting workorder line regarding the
reservation of the raw moves.
"""
rounding = move_line.product_uom_id.rounding
return self.filtered(lambda line:
line.lot_id == move_line.lot_id and
float_compare(line.qty_done, move_line.product_uom_qty, precision_rounding=rounding) < 0 and
line.product_id == move_line.product_id)
def _get_move_lines(self):
return self.move_id.move_line_ids.filtered(lambda ml:
ml.lot_id == self.lot_id and ml.product_id == self.product_id)

def _get_produced_lots(self):
return not self.is_produced and self._get_final_lots() and [(4, lot.id) for lot in self._get_final_lots()]
@@ -620,9 +620,9 @@ def test_product_produce_4(self):
product_produce.do_produce()

ml_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1).mapped('move_line_ids')
self.assertEqual(len(ml_p1), 4)
self.assertEqual(len(ml_p1), 3)
for ml in ml_p1:
self.assertIn(ml.qty_done, [1.0, 2.0], 'Quantity done should be 1.0, 2.0 or 3.0')
self.assertIn(ml.qty_done, [1.0, 2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0')
self.assertEqual(sum(ml_p1.mapped('qty_done')), 6.0, 'Total qty consumed should be 6.0')
self.assertEqual(sum(ml_p1.mapped('product_uom_qty')), 5.0, 'Total qty reserved should be 5.0')

@@ -684,6 +684,9 @@ def test_product_produce_6(self):
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1)
self.assertEqual(len(mo), 1, 'MO should have been created')

self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 4)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)

mo.action_assign()

produce_form = Form(self.env['mrp.product.produce'].with_context({
@@ -693,15 +696,134 @@ def test_product_produce_6(self):
produce_form.qty_producing = 3
self.assertEqual(len(produce_form.workorder_line_ids._records), 4, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_done'] for x in produce_form.workorder_line_ids._records]), 15, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_reserved'] for x in produce_form.workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
produce_form.qty_producing = 4
self.assertEqual(len(produce_form.workorder_line_ids._records), 6, 'Update the produce quantity should change the components quantity.')
self.assertEqual(len(produce_form.workorder_line_ids._records), 4, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_done'] for x in produce_form.workorder_line_ids._records]), 20, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_reserved'] for x in produce_form.workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')

produce_form.qty_producing = 1
self.assertEqual(len(produce_form.workorder_line_ids._records), 2, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_done'] for x in produce_form.workorder_line_ids._records]), 5, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_reserved'] for x in produce_form.workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
produce_wizard = produce_form.save()
produce_wizard.do_produce()

def test_product_produce_7(self):
""" Add components in 2 differents sub location. Do not reserve the MO
and checks that the move line created takes stock from location that
contains needed raw materials.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2)
self.assertEqual(len(mo), 1, 'MO should have been created')

self.stock_location = self.env.ref('stock.stock_location_stock')
self.stock_shelf_1 = self.env.ref('stock.stock_location_components')
self.stock_shelf_2 = self.env.ref('stock.stock_location_14')

self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 3)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 3)
self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 2)

self.env['stock.quant']._update_available_quantity(p2, self.stock_shelf_1, 1)
self.env['stock.quant']._update_available_quantity(p2, self.stock_shelf_2, 1)

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()

self.assertEqual(len(produce_wizard.workorder_line_ids), 2)
produce_wizard.do_produce()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1

produce_wizard = produce_form.save()

self.assertEqual(len(produce_wizard.workorder_line_ids), 2)
produce_wizard.do_produce()

mo.button_mark_done()
mo_move_line_p1 = mo.move_raw_ids[1].move_line_ids
self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_location).mapped('qty_done')), 3)
self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_shelf_1).mapped('qty_done')), 3)
self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_shelf_2).mapped('qty_done')), 2)
self.assertEqual(sum(mo.move_finished_ids.move_line_ids.mapped('qty_done')), 2)

self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_location, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_shelf_1, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_shelf_2, strict=True).quantity, 0)

self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_shelf_1, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_shelf_2, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 2)

def test_product_produce_8(self):
""" Produce more than reserved and planned. Check that produce wizard
only propose one line for product not reserved.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2)
self.assertEqual(len(mo), 1, 'MO should have been created')

self.stock_location = self.env.ref('stock.stock_location_stock')

self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 5)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2)

mo.action_assign()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
self.assertEqual(len(produce_wizard.workorder_line_ids), 2)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1).qty_reserved, 4)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p2).qty_reserved, 1)
produce_wizard.do_produce()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
# p1 1 1 1
# p1 3 0 3
# p2 1 1 1
self.assertEqual(len(produce_wizard.workorder_line_ids), 3)
self.assertEqual(sum(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1).mapped('qty_reserved')), 1)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1 and l.qty_reserved).qty_to_consume, 1)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1 and not l.qty_reserved).qty_to_consume, 3)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p2).qty_reserved, 1)

with Form(produce_wizard) as produce_form:
produce_form.qty_producing = 2
# p1 1 1 1
# p1 7 0 7
# p2 1 1 1
# p2 1 0 1
self.assertEqual(len(produce_wizard.workorder_line_ids), 4)
self.assertEqual(sum(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1).mapped('qty_reserved')), 1)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1 and l.qty_reserved).qty_to_consume, 1)
self.assertEqual(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p1 and not l.qty_reserved).qty_to_consume, 7)
self.assertEqual(sum(produce_wizard.workorder_line_ids.filtered(lambda l: l.product_id == p2).mapped('qty_reserved')), 1)

produce_wizard.do_produce()

mo.button_mark_done()

self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_location, strict=True).quantity, -7)
self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_location, strict=True).quantity, -1)
self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 3)

def test_product_produce_uom(self):
plastic_laminate = self.env.ref('mrp.product_product_plastic_laminate')
bom = self.env.ref('mrp.mrp_bom_plastic_laminate')
@@ -1261,8 +1261,10 @@ def _set_quantity_done(self, qty):
@param qty: quantity in the UoM of move.product_uom
"""
for ml in self.move_line_ids:
# Convert move line qty into move uom
ml_qty = ml.product_uom_qty - ml.qty_done
if float_compare(ml_qty, 0, precision_rounding=ml.product_uom_id.rounding) <= 0:
continue
# Convert move line qty into move uom
if ml.product_uom_id != self.product_uom:
ml_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)

@@ -1279,9 +1281,9 @@ def _set_quantity_done(self, qty):
taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)
qty -= taken_qty

if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0:
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0:
break
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) > 0:
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) > 0:
vals = self._prepare_move_line_vals(quantity=0)
vals['qty_done'] = qty
ml = self.env['stock.move.line'].create(vals)

0 comments on commit 1f09e18

Please sign in to comment.
You can’t perform that action at this time.