From d00d6bcb08f0897ba8bce7e1123adc40a18cbb44 Mon Sep 17 00:00:00 2001 From: Simon Wydooghe Date: Tue, 24 Oct 2023 17:13:41 +0200 Subject: [PATCH] [FIX] mrp: handle unbuild/scrap/unscrap and production with same serial When a product has been unbuilt and also (mistakenly) scrapped, it's impossible to reuse the serial number in production again, even if the product is unscrapped. This fixes the sanity check in a similar way to the _check_sn_uniqueness function, not just checking for removed stock move lines, but also unremoved stock move lines. A test case is also added to check this scenario. This is a similar fix to a previous commit we did (4f07b260807053586ac6c01cf92ac3d5e37b1041) where we bumped into a problem with the duplicate serial sanity check. closes odoo/odoo#142574 X-original-commit: 1761b95d9fda5d5355f35effd1f345a25ded1197 Signed-off-by: William Henrotin (whe) --- addons/mrp/models/mrp_production.py | 11 ++++- addons/mrp/tests/test_order.py | 73 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index e4175c09573..fb03df2b5ec 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -2560,10 +2560,17 @@ class MrpProduction(models.Model): removed = self.env['stock.move.line'].search_count([ ('lot_id', '=', lot.id), ('state', '=', 'done'), - ('location_dest_id.scrap_location', '=', True) + ('location_id.scrap_location', '=', False), + ('location_dest_id.scrap_location', '=', True), + ]) + unremoved = self.env['stock.move.line'].search_count([ + ('lot_id', '=', lot.id), + ('state', '=', 'done'), + ('location_id.scrap_location', '=', True), + ('location_dest_id.scrap_location', '=', False), ]) # Either removed or unbuild - if not ((duplicates_unbuild or removed) and duplicates - duplicates_unbuild - removed == 0): + if not ((duplicates_unbuild or removed) and duplicates - duplicates_unbuild - removed + unremoved == 0): return True # Check presence of same sn in current production duplicates = co_prod_move_lines.filtered(lambda ml: ml.quantity and ml.lot_id == lot) diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index d7e3c025470..babec6424cb 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -1391,6 +1391,79 @@ class TestMrpOrder(TestMrpCommon): subassembly_mo2.lot_producing_id = subassembly_sn subassembly_mo2.button_mark_done() + def test_product_produce_duplicate_6(self): + """Produce a product for the second time with the same serial + after having unbuilt, scrapped and unscrapped the product""" + product = self.env["product.product"].create( + { + "name": "Product", + "type": "product", + "tracking": "serial", + } + ) + + sn = self.env["stock.lot"].create( + { + "name": "SN", + "product_id": product.id, + "company_id": self.env.company.id, + } + ) + + mo1_form = Form(self.env["mrp.production"]) + mo1_form.product_id = product + mo1 = mo1_form.save() + mo1.action_confirm() + with Form(mo1) as mo: + mo.qty_producing = 1 + mo1.lot_producing_id = sn + mo1.button_mark_done() + + ub_form = Form(self.env["mrp.unbuild"]) + ub_form.mo_id = mo1 + ub = ub_form.save() + ub.action_unbuild() + + scrap = self.env['stock.scrap'].create({ + 'product_id': product.id, + 'product_uom_id': product.uom_id.id, + 'lot_id': sn.id, + }) + scrap.do_scrap() + + unscrap_picking = self.env['stock.picking'].create({ + 'picking_type_id': self.env.ref('stock.picking_type_internal').id, + 'location_id': scrap.scrap_location_id.id, + 'location_dest_id': scrap.location_id.id, + }) + unscrap_move = self.env['stock.move'].create({ + 'name': 'unscrap', + 'location_id': scrap.scrap_location_id.id, + 'location_dest_id': scrap.location_id.id, + 'product_id': product.id, + 'product_uom': product.uom_id.id, + 'picking_id': unscrap_picking.id, + }) + unscrap_picking.action_confirm() + self.env['stock.move.line'].create({ + 'move_id': unscrap_move.id, + 'product_id': unscrap_move.product_id.id, + 'lot_id': sn.id, + 'quantity': 1, + 'product_uom_id': unscrap_move.product_uom.id, + 'picking_id': unscrap_move.picking_id.id, + }) + unscrap_picking.button_validate() + + mo2_form = Form(self.env["mrp.production"]) + mo2_form.product_id = product + mo2 = mo2_form.save() + mo2.action_confirm() + with Form(mo2) as mo: + mo.qty_producing = 1 + mo2.lot_producing_id = sn + mo2.button_mark_done() + def test_product_produce_12(self): """ Checks that, the production is robust against deletion of finished move."""