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()