From 77ca3a830d0f61ee8a5ba31b3b79e80e85c08e79 Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Tue, 25 Jan 2022 13:48:29 +0000 Subject: [PATCH] [FIX] mrp: produce SN product with lot of unbuilt product To reproduce the issue: 1. Produce a SN product P with lot L 2. Unbuild it 3. Produce 1xP with lot L Error: When setting the lot, an error is displayed. When marking the MO as done, a second error is displayed Both domains in `_onchange_lot_producing` and in the beginning of `_check_sn_uniqueness` are incorrect. We should centralize the uniqueness checking thanks to all existing computations in `_check_sn_uniqueness`. This way, unbuilt/scrapped moves will be considered OPW-2721205 closes odoo/odoo#84374 X-original-commit: 2540104e95cea7baaa22ccfce97759771dbbaf34 Signed-off-by: Tiffany Chang Signed-off-by: Adrien Widart --- addons/mrp/models/mrp_production.py | 79 +++++++++++++-------------- addons/mrp/tests/test_traceability.py | 48 ++++++++++++++++ 2 files changed, 85 insertions(+), 42 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 15b2dabd2d6..b53517a332e 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -2008,54 +2008,16 @@ class MrpProduction(models.Model): def _check_sn_uniqueness(self): """ Alert the user if the serial number as already been consumed/produced """ if self.product_tracking == 'serial' and self.lot_producing_id: - sml = self.env['stock.move.line'].search_count([ - ('lot_id', '=', self.lot_producing_id.id), - ('location_id.usage', '=', 'production'), - ('qty_done', '=', 1), - ('state', '=', 'done') - ]) - if sml: + if self._is_finished_sn_already_produced(self.lot_producing_id): raise UserError(_('This serial number for product %s has already been produced', self.product_id.name)) for move in self.move_finished_ids: if move.has_tracking != 'serial' or move.product_id == self.product_id: continue for move_line in move.move_line_ids: - domain = [ - ('lot_id', '=', move_line.lot_id.id), - ('qty_done', '=', 1), - ('state', '=', 'done') - ] - message = _('The serial number %(number)s used for byproduct %(product_name)s has already been produced', - number=move_line.lot_id.name, - product_name=move_line.product_id.name) - co_prod_move_lines = self.move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self.product_id) - domain_unbuild = domain + [ - ('production_id', '=', False), - ('location_dest_id.usage', '=', 'production') - ] - - # Check presence of same sn in previous productions - duplicates = self.env['stock.move.line'].search_count(domain + [ - ('location_id.usage', '=', 'production') - ]) - if duplicates: - # Maybe some move lines have been compensated by unbuild - duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [ - ('move_id.unbuild_id', '!=', False) - ]) - removed = self.env['stock.move.line'].search_count([ - ('lot_id', '=', move_line.lot_id.id), - ('state', '=', 'done'), - ('location_dest_id.scrap_location', '=', True) - ]) - # Either removed or unbuild - if not ((duplicates_unbuild or removed) and duplicates - duplicates_unbuild - removed == 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 - if duplicates: - raise UserError(message) + if self._is_finished_sn_already_produced(move_line.lot_id, excluded_sml=move_line): + raise UserError(_('The serial number %(number)s used for byproduct %(product_name)s has already been produced', + number=move_line.lot_id.name, product_name=move_line.product_id.name)) for move in self.move_raw_ids: if move.has_tracking != 'serial': @@ -2091,6 +2053,39 @@ class MrpProduction(models.Model): if duplicates: raise UserError(message) + def _is_finished_sn_already_produced(self, lot, excluded_sml=None): + excluded_sml = excluded_sml or self.env['stock.move.line'] + domain = [ + ('lot_id', '=', lot.id), + ('qty_done', '=', 1), + ('state', '=', 'done') + ] + co_prod_move_lines = self.move_finished_ids.move_line_ids - excluded_sml + domain_unbuild = domain + [ + ('production_id', '=', False), + ('location_dest_id.usage', '=', 'production') + ] + # Check presence of same sn in previous productions + duplicates = self.env['stock.move.line'].search_count(domain + [ + ('location_id.usage', '=', 'production') + ]) + if duplicates: + # Maybe some move lines have been compensated by unbuild + duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [ + ('move_id.unbuild_id', '!=', False) + ]) + removed = self.env['stock.move.line'].search_count([ + ('lot_id', '=', lot.id), + ('state', '=', 'done'), + ('location_dest_id.scrap_location', '=', True) + ]) + # Either removed or unbuild + if not ((duplicates_unbuild or removed) and duplicates - duplicates_unbuild - removed == 0): + return True + # Check presence of same sn in current production + duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == lot) + return bool(duplicates) + def _check_immediate(self): immediate_productions = self.browse() if self.env.context.get('skip_immediate'): diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index b10080253f8..dbce7c8f52d 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -3,6 +3,9 @@ from odoo.tests import Form from odoo.addons.mrp.tests.common import TestMrpCommon +import logging + +_logger = logging.getLogger(__name__) class TestTraceability(TestMrpCommon): @@ -319,3 +322,48 @@ class TestTraceability(TestMrpCommon): self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2') self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2) + + def test_reuse_unbuilt_usn(self): + """ + Produce a SN product + Unbuilt it + Produce a new SN product with same lot + """ + mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=1, qty_base_2=1, qty_final=1, tracking_final='serial') + stock_location = self.env.ref('stock.stock_location_stock') + self.env['stock.quant']._update_available_quantity(p1, stock_location, 1) + self.env['stock.quant']._update_available_quantity(p2, stock_location, 1) + mo.action_assign() + + lot = self.env['stock.lot'].create({ + 'name': 'lot1', + 'product_id': p_final.id, + 'company_id': self.env.company.id, + }) + + mo_form = Form(mo) + mo_form.qty_producing = 1.0 + mo_form.lot_producing_id = lot + mo = mo_form.save() + mo.button_mark_done() + + unbuild_form = Form(self.env['mrp.unbuild']) + unbuild_form.mo_id = mo + unbuild_form.lot_id = lot + unbuild_form.save().action_unbuild() + + mo_form = Form(self.env['mrp.production']) + mo_form.bom_id = bom + mo = mo_form.save() + mo.action_confirm() + + with self.assertLogs(level="WARNING") as log_catcher: + mo_form = Form(mo) + mo_form.qty_producing = 1.0 + mo_form.lot_producing_id = lot + mo = mo_form.save() + _logger.warning('Dummy') + self.assertEqual(len(log_catcher.output), 1, "Useless warnings: \n%s" % "\n".join(log_catcher.output[:-1])) + + mo.button_mark_done() + self.assertEqual(mo.state, 'done')