From a105e7f7fe7fbd68a8389a598f68899d3f3aa3ae Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 3 Jan 2023 18:27:53 +0100 Subject: [PATCH] Allow to propagate lot from multiple BOM lines for variants When defining a single BOM for a product template having multiple product variants, we can have different components where the lot number must be propagated for different product variants. Therefore we need to allow to mark multiple BOM line with propagate_lot_number, and to avoid complicating the check function on the BOM lines, we ensure at the manufacturing order confirmation that only a single component is set to propagate its lot number. --- .../models/mrp_bom_line.py | 10 --- .../models/mrp_production.py | 28 +++++++- mrp_lot_number_propagation/tests/common.py | 71 ++++++++++++++++++- .../tests/test_mrp_bom.py | 11 --- .../tests/test_mrp_production.py | 59 ++++++++++++++- 5 files changed, 152 insertions(+), 27 deletions(-) diff --git a/mrp_lot_number_propagation/models/mrp_bom_line.py b/mrp_lot_number_propagation/models/mrp_bom_line.py index b8b02a403..b0f8d97f0 100644 --- a/mrp_lot_number_propagation/models/mrp_bom_line.py +++ b/mrp_lot_number_propagation/models/mrp_bom_line.py @@ -39,16 +39,6 @@ class MrpBomLine(models.Model): for line in self: if not line.bom_id.lot_number_propagation: continue - lines_to_propagate = line.bom_id.bom_line_ids.filtered( - lambda o: o.propagate_lot_number - ) - if len(lines_to_propagate) > 1: - raise ValidationError( - _( - "Only one BoM line can propagate its lot/serial number " - "to the finished product." - ) - ) if line.propagate_lot_number and line.product_id.tracking != "serial": raise ValidationError( _( diff --git a/mrp_lot_number_propagation/models/mrp_production.py b/mrp_lot_number_propagation/models/mrp_production.py index 3bc066377..d1afe8a98 100644 --- a/mrp_lot_number_propagation/models/mrp_production.py +++ b/mrp_lot_number_propagation/models/mrp_production.py @@ -70,9 +70,31 @@ class MrpProduction(models.Model): def _set_lot_number_propagation_data_from_bom(self): """Copy information from BoM to the manufacturing order.""" for order in self: - order.is_lot_number_propagated = order.bom_id.lot_number_propagation - for move in order.move_raw_ids: - move.propagate_lot_number = move.bom_line_id.propagate_lot_number + propagate_lot = order.bom_id.lot_number_propagation + if not propagate_lot: + continue + order.is_lot_number_propagated = propagate_lot + propagate_move = order.move_raw_ids.filtered( + lambda m: m.bom_line_id.propagate_lot_number + ) + if not propagate_move: + raise UserError( + _( + "Bill of material is marked for lot number propagation, but " + "there are no components propagating lot number. " + "Please check BOM configuration." + ) + ) + elif len(propagate_move) > 1: + raise UserError( + _( + "Bill of material is marked for lot number propagation, but " + "there are multiple components propagating lot number. " + "Please check BOM configuration." + ) + ) + else: + propagate_move.propagate_lot_number = True def _post_inventory(self, cancel_backorder=False): self._create_and_assign_propagated_lot_number() diff --git a/mrp_lot_number_propagation/tests/common.py b/mrp_lot_number_propagation/tests/common.py index 9a81d7d85..19194492e 100644 --- a/mrp_lot_number_propagation/tests/common.py +++ b/mrp_lot_number_propagation/tests/common.py @@ -5,7 +5,7 @@ import random import string from odoo import fields -from odoo.tests import common +from odoo.tests import Form, common class Common(common.TransactionCase): @@ -17,12 +17,19 @@ class Common(common.TransactionCase): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.bom = cls.env.ref("mrp.mrp_bom_desk") + cls.bom_product_template = cls.env.ref( + "mrp.product_product_computer_desk_product_template" + ) + cls.bom_product_product = cls.env.ref("mrp.product_product_computer_desk") cls.product_tracked_by_lot = cls.env.ref( "mrp.product_product_computer_desk_leg" ) cls.product_tracked_by_sn = cls.env.ref( "mrp.product_product_computer_desk_head" ) + cls.product_template_tracked_by_sn = cls.env.ref( + "mrp.product_product_computer_desk_head_product_template" + ) cls.line_tracked_by_lot = cls.bom.bom_line_ids.filtered( lambda o: o.product_id == cls.product_tracked_by_lot ) @@ -90,3 +97,65 @@ class Common(common.TransactionCase): lambda q: q.location_id.parent_path in location.parent_path ) return quants + + @classmethod + def _add_color_and_legs_variants(cls, product_template): + color_attribute = cls.env.ref("product.product_attribute_2") + color_att_value_white = cls.env.ref("product.product_attribute_value_3") + color_att_value_black = cls.env.ref("product.product_attribute_value_4") + legs_attribute = cls.env.ref("product.product_attribute_1") + legs_att_value_steel = cls.env.ref("product.product_attribute_value_1") + legs_att_value_alu = cls.env.ref("product.product_attribute_value_2") + cls._add_variants( + product_template, + { + color_attribute: [color_att_value_white, color_att_value_black], + legs_attribute: [legs_att_value_steel, legs_att_value_alu], + }, + ) + + @classmethod + def _add_variants(cls, product_template, attribute_values_dict): + for attribute, att_values_list in attribute_values_dict.items(): + cls.env["product.template.attribute.line"].create( + { + "product_tmpl_id": product_template.id, + "attribute_id": attribute.id, + "value_ids": [ + fields.Command.set([att_val.id for att_val in att_values_list]) + ], + } + ) + + @classmethod + def _create_bom_with_variants(cls): + attribute_values_dict = { + att_val.product_attribute_value_id.name: att_val.id + for att_val in cls.env["product.template.attribute.value"].search( + [("product_tmpl_id", "=", cls.bom_product_template.id)] + ) + } + new_bom_form = Form(cls.env["mrp.bom"]) + new_bom_form.product_tmpl_id = cls.bom_product_template + new_bom = new_bom_form.save() + bom_line_create_values = [] + for product in cls.product_template_tracked_by_sn.product_variant_ids: + create_values = {"bom_id": new_bom.id} + create_values["product_id"] = product.id + att_values_commands = [] + for att_value in product.product_template_attribute_value_ids: + att_values_commands.append( + fields.Command.link(attribute_values_dict[att_value.name]) + ) + create_values[ + "bom_product_template_attribute_value_ids" + ] = att_values_commands + bom_line_create_values.append(create_values) + cls.env["mrp.bom.line"].create(bom_line_create_values) + new_bom_form = Form(new_bom) + new_bom_form.lot_number_propagation = True + for line_position, _bom_line in enumerate(new_bom.bom_line_ids): + new_bom_line_form = new_bom_form.bom_line_ids.edit(line_position) + new_bom_line_form.propagate_lot_number = True + new_bom_line_form.save() + return new_bom_form.save() diff --git a/mrp_lot_number_propagation/tests/test_mrp_bom.py b/mrp_lot_number_propagation/tests/test_mrp_bom.py index 18a4604f7..6b740ab19 100644 --- a/mrp_lot_number_propagation/tests/test_mrp_bom.py +++ b/mrp_lot_number_propagation/tests/test_mrp_bom.py @@ -13,17 +13,6 @@ class TestMrpBom(Common): self.bom.product_tmpl_id.tracking = "none" self.assertFalse(self.bom.display_lot_number_propagation) - def test_bom_line_check_propagate_lot_number_multi(self): - form = Form(self.bom) - form.lot_number_propagation = True - # Flag more than one line to propagate - for i in range(len(form.bom_line_ids)): - line_form = form.bom_line_ids.edit(i) - line_form.propagate_lot_number = True - line_form.save() - with self.assertRaisesRegex(ValidationError, "Only one BoM"): - form.save() - def test_bom_line_check_propagate_lot_number_not_tracked(self): form = Form(self.bom) form.lot_number_propagation = True diff --git a/mrp_lot_number_propagation/tests/test_mrp_production.py b/mrp_lot_number_propagation/tests/test_mrp_production.py index ffbad6446..90d22ede9 100644 --- a/mrp_lot_number_propagation/tests/test_mrp_production.py +++ b/mrp_lot_number_propagation/tests/test_mrp_production.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) from odoo.exceptions import UserError +from odoo.fields import Command from odoo.tests.common import Form from .common import Common @@ -12,15 +13,24 @@ class TestMrpProduction(Common): def setUpClass(cls): super().setUpClass() # Configure the BoM to propagate lot number + cls._configure_bom() + cls.order = cls._create_order(cls.bom_product_product, cls.bom) + + @classmethod + def _configure_bom(cls): with Form(cls.bom) as form: form.lot_number_propagation = True line_form = form.bom_line_ids.edit(0) # Line tracked by SN line_form.propagate_lot_number = True line_form.save() form.save() + + @classmethod + def _create_order(cls, product, bom): with Form(cls.env["mrp.production"]) as form: - form.bom_id = cls.bom - cls.order = form.save() + form.product_id = product + form.bom_id = bom + return form.save() def _set_qty_done(self, order): for line in order.move_raw_ids.move_line_ids: @@ -46,3 +56,48 @@ class TestMrpProduction(Common): self._set_qty_done(self.order) self.order.button_mark_done() self.assertEqual(self.order.lot_producing_id.name, self.LOT_NAME) + + def test_confirm_with_variant_ok(self): + self._add_color_and_legs_variants(self.bom_product_template) + self._add_color_and_legs_variants(self.product_template_tracked_by_sn) + new_bom = self._create_bom_with_variants() + self.assertTrue(new_bom.lot_number_propagation) + # As all variants must have a single component + # where lot must be propagated, there should not be any error + for product in self.bom_product_template.product_variant_ids: + new_order = self._create_order(product, new_bom) + new_order.action_confirm() + + def test_confirm_with_variant_multiple(self): + self._add_color_and_legs_variants(self.bom_product_template) + self._add_color_and_legs_variants(self.product_template_tracked_by_sn) + new_bom = self._create_bom_with_variants() + # Remove application on variant for first bom line + # with this only the first variant of the product template + # will have a single component where lot must be propagated + new_bom.bom_line_ids[0].bom_product_template_attribute_value_ids = [ + Command.clear() + ] + for cnt, product in enumerate(self.bom_product_template.product_variant_ids): + new_order = self._create_order(product, new_bom) + if cnt == 0: + new_order.action_confirm() + else: + with self.assertRaisesRegex(UserError, "multiple components"): + new_order.action_confirm() + + def test_confirm_with_variant_no(self): + self._add_color_and_legs_variants(self.bom_product_template) + self._add_color_and_legs_variants(self.product_template_tracked_by_sn) + new_bom = self._create_bom_with_variants() + # Remove first bom line + # with this the first variant of the product template + # will not have any component where lot must be propagated + new_bom.bom_line_ids[0].unlink() + for cnt, product in enumerate(self.bom_product_template.product_variant_ids): + new_order = self._create_order(product, new_bom) + if cnt == 0: + with self.assertRaisesRegex(UserError, "no component"): + new_order.action_confirm() + else: + new_order.action_confirm()