From ac11b7e00dea13a0d20bf0c39e544d12c0a1e9fe Mon Sep 17 00:00:00 2001 From: "Adrien Widart (awt)" <awt@odoo.com> Date: Tue, 15 Nov 2022 15:08:40 +0000 Subject: [PATCH] [FIX] mrp: include unscrapped SN when checking uniqueness It is not possible to consume a component tracked by serial that comes back from a scrap location To reproduce the issue: 1. In Settings, enable "Multi Routes" 2. Create two storable products P_compo, P_finished - P_compo is tracked by serial number 3. Update the on-hand qty of P_compo: - 1 x P_compo with serial SN 4. Process a manufacturing order MO: - Product: P_finished - Compo: 1 x P_compo with SN 5. Unbuild P_finished - It brings SN back to stock 5. Scrap one P_compo with SN 6. Unscrap it (thanks to an internal transfer) 7. Repeat step 4 Error: a user error is raised: "The serial number SN used for component P_compo has already been consumed" When checking the SN uniqueness of a component, we don't consider the case where a product came back from a srap location OPW-3055252 closes odoo/odoo#105843 Signed-off-by: William Henrotin (whe) <whe@odoo.com> --- addons/mrp/models/mrp_production.py | 8 ++- addons/mrp/tests/test_traceability.py | 87 +++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index c3c1b83d7d96..97f25d82f111 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -1795,8 +1795,14 @@ class MrpProduction(models.Model): ('state', '=', 'done'), ('location_dest_id.scrap_location', '=', True) ]) + unremoved = self.env['stock.move.line'].search_count([ + ('lot_id', '=', move_line.lot_id.id), + ('state', '=', 'done'), + ('location_id.scrap_location', '=', True), + ('location_dest_id.scrap_location', '=', False), + ]) # Either removed or unbuild - if not ((duplicates_returned or removed) and duplicates - duplicates_returned - removed == 0): + if not ((duplicates_returned or removed) and duplicates - duplicates_returned - removed + unremoved == 0): raise UserError(message) # Check presence of same sn in current production duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index 9cae9394f436..40b97f2677cf 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -497,3 +497,90 @@ class TestTraceability(TestMrpCommon): mo.button_mark_done() self.assertEqual(mo.state, 'done') + + def test_unbuild_scrap_and_unscrap_tracked_component(self): + """ + Suppose a tracked-by-SN component C. There is one C in stock with SN01. + Build a product P that uses C with SN, unbuild P, scrap SN, unscrap SN + and rebuild a product with SN in the components + """ + warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1) + stock_location = warehouse.lot_stock_id + + component = self.bom_4.bom_line_ids.product_id + component.write({ + 'type': 'product', + 'tracking': 'serial', + }) + serial_number = self.env['stock.production.lot'].create({ + 'product_id': component.id, + 'name': 'Super Serial', + 'company_id': self.env.company.id, + }) + self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=serial_number) + + # produce 1 + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = self.bom_4 + mo = mo_form.save() + mo.action_confirm() + mo.action_assign() + self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number) + + with Form(mo) as mo_form: + mo_form.qty_producing = 1 + mo.move_raw_ids.move_line_ids.qty_done = 1 + mo.button_mark_done() + + # unbuild + action = mo.button_unbuild() + wizard = Form(self.env[action['res_model']].with_context(action['context'])).save() + wizard.action_validate() + + # scrap the component + scrap = self.env['stock.scrap'].create({ + 'product_id': component.id, + 'product_uom_id': component.uom_id.id, + 'scrap_qty': 1, + 'lot_id': serial_number.id, + }) + scrap_location = scrap.scrap_location_id + scrap.do_scrap() + + # unscrap the component + internal_move = self.env['stock.move'].create({ + 'name': component.name, + 'location_id': scrap_location.id, + 'location_dest_id': stock_location.id, + 'product_id': component.id, + 'product_uom': component.uom_id.id, + 'product_uom_qty': 1.0, + 'move_line_ids': [(0, 0, { + 'product_id': component.id, + 'location_id': scrap_location.id, + 'location_dest_id': stock_location.id, + 'product_uom_id': component.uom_id.id, + 'qty_done': 1.0, + 'lot_id': serial_number.id, + })], + }) + internal_move._action_confirm() + internal_move._action_done() + + # produce one with the unscrapped component + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = self.bom_4 + mo = mo_form.save() + mo.action_confirm() + mo.action_assign() + self.assertEqual(mo.move_raw_ids.move_line_ids.lot_id, serial_number) + + with Form(mo) as mo_form: + mo_form.qty_producing = 1 + mo.move_raw_ids.move_line_ids.qty_done = 1 + mo.button_mark_done() + + self.assertRecordValues((mo.move_finished_ids + mo.move_raw_ids).move_line_ids, [ + {'product_id': self.bom_4.product_id.id, 'lot_id': False, 'qty_done': 1}, + {'product_id': component.id, 'lot_id': serial_number.id, 'qty_done': 1}, + ]) -- GitLab