From b622f3b9b161b92bb6e56669e00fb6d41ef2f646 Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Wed, 13 Apr 2022 13:00:44 +0000 Subject: [PATCH] [FIX] mrp: produce a tracked and manufactured component In some cases, it is not possible to produce a tracked product that has been used as a component of another product. To reproduce the issue: 1. Create three products A, B, C: - A storable - B storable and tracked by lot - C consumable 2. Update on-hand qty of B - 10 x lot L01 - 5 x lot L02 3. Create two bills of materials - Product: A - Components: 1 x B - Product: B - Components: 1 x C 4. Produce 15 x A (MO01) - Note: it should use 10 L01 and 5 L02 5. Produce 15 x B with lot L03 (MO02) Error: When marking MO02 as done, a user error is displayed: "Cannot set the done quantity from this stock move, work directly with the move lines." The user should be able to mark the MO as done. When confirming MO02, it confirms the associated SMs. Since the conditions are respected, we try to assign both consumed and finished moves. Because of step 4, there are two quants for the product B in the production location (one for L01 and another for L02). Therefore, when reserving the qty of the finished move, two SML are generated. As a result, when marking MO02 as done, we write the `quantity_done` on the finished move. This triggers the inverse method and leads to `_multi_line_quantity_done_set`, where the error is raised because the done quantities are not defined on each SML: https://github.com/odoo/odoo/blob/1354081e43b71edbd434dccc6df6d0881c672a81/addons/stock/models/stock_move.py#L363-L364 Thanks to `_set_quantity_done`, we can write these done quantities on each SML (and then the incorrect lots will be replaced by the producing lot). OPW-2752436 closes odoo/odoo#89015 X-original-commit: 65efb3ec5416fab6bb9ba0a015565b2abb3928cd Signed-off-by: William Henrotin (whe) Signed-off-by: Adrien Widart --- addons/mrp/models/mrp_production.py | 4 +- addons/mrp/tests/test_traceability.py | 67 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index c2e1c00d477..629bb7a55b2 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -1471,8 +1471,8 @@ class MrpProduction(models.Model): for order in self: finish_moves = order.move_finished_ids.filtered(lambda m: m.product_id == order.product_id and m.state not in ('done', 'cancel')) # the finish move can already be completed by the workorder. - if not finish_moves.quantity_done: - finish_moves.quantity_done = float_round(order.qty_producing - order.qty_produced, precision_rounding=order.product_uom_id.rounding, rounding_method='HALF-UP') + if finish_moves and not finish_moves.quantity_done: + finish_moves._set_quantity_done(float_round(order.qty_producing - order.qty_produced, precision_rounding=order.product_uom_id.rounding, rounding_method='HALF-UP')) finish_moves.move_line_ids.lot_id = order.lot_producing_id order._cal_price(moves_to_do_by_order[order.id]) moves_to_finish = self.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index dbce7c8f52d..bf35aa22abb 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -367,3 +367,70 @@ class TestTraceability(TestMrpCommon): mo.button_mark_done() self.assertEqual(mo.state, 'done') + + def test_tracked_and_manufactured_component(self): + """ + Suppose this structure: + productA --|- 1 x productB --|- 1 x productC + with productB tracked by lot + Ensure that, when we already have some qty of productB (with different lots), + the user can produce several productA and can then produce some productB again + """ + stock_location = self.env.ref('stock.stock_location_stock') + + productA, productB, productC = self.env['product.product'].create([{ + 'name': 'Product A', + 'type': 'product', + }, { + 'name': 'Product B', + 'type': 'product', + 'tracking': 'lot', + }, { + 'name': 'Product C', + 'type': 'consu', + }]) + + lot_B01, lot_B02, lot_B03 = self.env['stock.lot'].create([{ + 'name': 'lot %s' % i, + 'product_id': productB.id, + 'company_id': self.env.company.id, + } for i in [1, 2, 3]]) + + self.env['mrp.bom'].create([{ + 'product_id': finished.id, + 'product_tmpl_id': finished.product_tmpl_id.id, + 'product_uom_id': self.uom_unit.id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})], + } for finished, component in [(productA, productB), (productB, productC)]]) + + self.env['stock.quant']._update_available_quantity(productB, stock_location, 10, lot_id=lot_B01) + self.env['stock.quant']._update_available_quantity(productB, stock_location, 5, lot_id=lot_B02) + + # Produce 15 x productA + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = productA + mo_form.product_qty = 15 + mo = mo_form.save() + mo.action_confirm() + action = mo.button_mark_done() + wizard = Form(self.env[action['res_model']].with_context(action['context'])).save() + wizard.process() + + # Produce 15 x productB + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = productB + mo_form.product_qty = 15 + mo = mo_form.save() + mo.action_confirm() + mo_form = Form(mo) + mo_form.qty_producing = 15 + mo_form.lot_producing_id = lot_B03 + mo = mo_form.save() + mo.button_mark_done() + + self.assertEqual(lot_B01.product_qty, 0) + self.assertEqual(lot_B02.product_qty, 0) + self.assertEqual(lot_B03.product_qty, 15) + self.assertEqual(productA.qty_available, 15)