From d9b9f94d18159e75a9baf0c97db2afb82bc05359 Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Wed, 23 Feb 2022 12:38:13 +0000 Subject: [PATCH] [FIX] mrp: unbuild MO with several lines for same component When unbuilding a MO, if the latter has several lines (in its components) for the same tracked product, it won't work. To reproduce the issue: 1. Create two storable products P_compo and P_finished - P_compo is tracked by USN 2. Update P_compo's quantity to 2 3. Create a MO: - Product: Finished - Quantity: 1 - Components: - 1 x P_compo - 1 x P_compo 4. Process the MO 5. Unbuild the MO Error: a validation error is displayed while it should not "The serial number has already been assigned: Product: P_Compo, Serial Number: C01" The for-loop in `action_unbuild` do not handle the case where there are several SMs for the same product. In the above case, there are two component lines for P_compo, so there are two SM in `MO.move_raw_ids`. When unbuilding the MO, the module creates and processes two SMs (SM01, SM02) that "produce" two P_compo (stored in `produce_moves`). When processing the first one in the for-loop, it takes the original moves (so the `MO.move_raw_ids` with the same product) and uses them one by one. Since SM01 needs one P_compo, it uses the first SM of `MO.move_raw_ids` (and reuse the lot of that SM). Later on, when processing SM02 in the for-loop, it does exactly the same thing: it takes the original moves and uses the first one. Here is the issue: this first SM has already been used and so does its lot, so this SM should actually not be used again. OPW-2715155 closes odoo/odoo#85354 X-original-commit: 8d2597965202a76ddeea8d8fed60b7ac014e2df8 Signed-off-by: Steve Van Essche Signed-off-by: Adrien Widart --- addons/mrp/models/mrp_unbuild.py | 6 +++- addons/mrp/tests/test_unbuild.py | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index 6dd4f0a55c5..c89c0372b3c 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -6,6 +6,8 @@ from odoo.exceptions import AccessError, UserError from odoo.tools import float_compare, float_round from odoo.osv import expression +from collections import defaultdict + class MrpUnbuild(models.Model): _name = "mrp.unbuild" @@ -159,6 +161,7 @@ class MrpUnbuild(models.Model): finished_move.quantity_done = finished_move.product_uom_qty # TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took + qty_already_used = defaultdict(float) for move in produce_moves | consume_moves: if move.has_tracking != 'none': original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids @@ -169,7 +172,7 @@ class MrpUnbuild(models.Model): moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.produce_line_ids.lot_id) # FIXME sle: double check with arm for move_line in moves_lines: # Iterate over all move_lines until we unbuilded the correct quantity. - taken_quantity = min(needed_quantity, move_line.qty_done) + taken_quantity = min(needed_quantity, move_line.qty_done - qty_already_used[move_line]) if taken_quantity: self.env['stock.move.line'].create({ 'move_id': move.id, @@ -181,6 +184,7 @@ class MrpUnbuild(models.Model): 'location_dest_id': move.location_dest_id.id, }) needed_quantity -= taken_quantity + qty_already_used[move_line] += taken_quantity else: move.quantity_done = float_round(move.product_uom_qty, precision_rounding=move.product_uom.rounding) diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py index dee22c6a46e..784f99c5404 100644 --- a/addons/mrp/tests/test_unbuild.py +++ b/addons/mrp/tests/test_unbuild.py @@ -631,3 +631,59 @@ class TestUnbuild(TestMrpCommon): uo = uo_form.save() uo.action_unbuild() self.assertEqual(uo.state, 'done') + + def test_unbuild_similar_tracked_components(self): + """ + Suppose a MO with, in the components, two lines for the same tracked-by-usn product + When unbuilding such an MO, all SN used in the MO should be back in stock + """ + compo, finished = self.env['product.product'].create([{ + 'name': 'compo', + 'type': 'product', + 'tracking': 'serial', + }, { + 'name': 'finished', + 'type': 'product', + }]) + + lot01, lot02 = self.env['stock.lot'].create([{ + 'name': n, + 'product_id': compo.id, + 'company_id': self.env.company.id, + } for n in ['lot01', 'lot02']]) + self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot01) + self.env['stock.quant']._update_available_quantity(compo, self.stock_location, 1, lot_id=lot02) + + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = finished + with mo_form.move_raw_ids.new() as line: + line.product_id = compo + line.product_uom_qty = 1 + with mo_form.move_raw_ids.new() as line: + line.product_id = compo + line.product_uom_qty = 1 + mo = mo_form.save() + + mo.action_confirm() + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() + mo.action_assign() + + details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 1 + details_operation_form.save() + mo.button_mark_done() + + uo_form = Form(self.env['mrp.unbuild']) + uo_form.mo_id = mo + uo_form.product_qty = 1 + uo = uo_form.save() + uo.action_unbuild() + + self.assertEqual(uo.produce_line_ids.filtered(lambda sm: sm.product_id == compo).lot_ids, lot01 + lot02)