diff --git a/mrp_bom_attribute_match/__manifest__.py b/mrp_bom_attribute_match/__manifest__.py
index 5c23306cf..e37195ffe 100644
--- a/mrp_bom_attribute_match/__manifest__.py
+++ b/mrp_bom_attribute_match/__manifest__.py
@@ -11,4 +11,5 @@
"data": [
"views/mrp_bom_views.xml",
],
+ "demo": ["demo/product_product_demo.xml"],
}
diff --git a/mrp_bom_attribute_match/demo/product_product_demo.xml b/mrp_bom_attribute_match/demo/product_product_demo.xml
new file mode 100644
index 000000000..522972ce5
--- /dev/null
+++ b/mrp_bom_attribute_match/demo/product_product_demo.xml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+ Top Level
+ product
+
+
+
+
+
+
+
+ Sub Level
+ product
+
+
+
+
+
+
+
+ Sub Sub
+ product
+
+
+
+
+
+
+
+ Sub Sub 2
+ product
+
+
+
+
+
+
+
+
+ attr1
+ 10
+
+
+ office
+
+ 1
+
+
+ gaming
+
+ 1
+
+
+
+ attr2
+ 10
+
+
+ v1
+
+ 1
+
+
+ v2
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
diff --git a/mrp_bom_attribute_match/reports/mrp_report_bom_structure.py b/mrp_bom_attribute_match/reports/mrp_report_bom_structure.py
index 4574d1d3c..7861e86ec 100644
--- a/mrp_bom_attribute_match/reports/mrp_report_bom_structure.py
+++ b/mrp_bom_attribute_match/reports/mrp_report_bom_structure.py
@@ -1,8 +1,8 @@
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-
from odoo import models
+from odoo.tools import float_round
class ReportBomStructure(models.AbstractModel):
@@ -32,6 +32,9 @@ class ReportBomStructure(models.AbstractModel):
if to_ignore_line_ids:
for to_ignore_line_id in to_ignore_line_ids:
bom.bom_line_ids = [(3, to_ignore_line_id, 0)]
+ product = bom._get_component_template_product(
+ line, product, line.product_id
+ )
components, total = super()._get_bom_lines(
bom, bom_quantity, product, line_id, level
)
@@ -43,3 +46,69 @@ class ReportBomStructure(models.AbstractModel):
if isinstance(value, models.NewId):
component[key] = value.origin
return components, total
+
+ def _get_price(self, bom, factor, product):
+ """Replaced in order to implement component_template logic"""
+ price = 0
+ if bom.operation_ids:
+ # routing are defined on a BoM and don't have a concept of quantity.
+ # It means that the operation time are defined for the quantity on
+ # the BoM (the user produces a batch of products). E.g the user
+ # product a batch of 10 units with a 5 minutes operation, the time
+ # will be the 5 for a quantity between 1-10, then doubled for
+ # 11-20,...
+ operation_cycle = float_round(
+ factor, precision_rounding=1, rounding_method="UP"
+ )
+ operations = self._get_operation_line(bom, operation_cycle, 0)
+ price += sum([op["total"] for op in operations])
+
+ for line in bom.bom_line_ids:
+ if line._skip_bom_line(product):
+ continue
+ if line.child_bom_id:
+ qty = line.product_uom_id._compute_quantity(
+ line.product_qty * (factor / bom.product_qty),
+ line.child_bom_id.product_uom_id,
+ )
+ sub_price = self._get_price(line.child_bom_id, qty, line.product_id)
+ price += sub_price
+ else:
+ prod_qty = line.product_qty * factor / bom.product_qty
+ company = bom.company_id or self.env.company
+ # Modification start
+ if line.component_template_id:
+ vals = product.product_template_attribute_value_ids.mapped(
+ "product_attribute_value_id"
+ ).ids
+ match_found = False
+ for prod in line.component_template_id.product_variant_ids:
+ pavs = prod.product_template_attribute_value_ids.mapped(
+ "product_attribute_value_id"
+ )
+ match = set(pavs.ids).issubset(set(vals))
+ if match:
+ match_found = True
+ break
+ if match_found:
+ not_rounded_price = (
+ prod.uom_id._compute_price(
+ prod.with_company(company).standard_price,
+ line.product_uom_id,
+ )
+ * prod_qty
+ )
+ price += company.currency_id.round(not_rounded_price)
+ else:
+ continue
+ # Modification end
+ else:
+ not_rounded_price = (
+ line.product_id.uom_id._compute_price(
+ line.product_id.with_company(company).standard_price,
+ line.product_uom_id,
+ )
+ * prod_qty
+ )
+ price += company.currency_id.round(not_rounded_price)
+ return price
diff --git a/mrp_bom_attribute_match/tests/__init__.py b/mrp_bom_attribute_match/tests/__init__.py
index 9b6b0f074..95a3f2121 100644
--- a/mrp_bom_attribute_match/tests/__init__.py
+++ b/mrp_bom_attribute_match/tests/__init__.py
@@ -1 +1,2 @@
from . import test_mrp_bom_attribute_match
+from . import test_mrp_bom_attribute_match_nested
diff --git a/mrp_bom_attribute_match/tests/common.py b/mrp_bom_attribute_match/tests/common.py
index 5307b8e18..eb32bd8e0 100644
--- a/mrp_bom_attribute_match/tests/common.py
+++ b/mrp_bom_attribute_match/tests/common.py
@@ -1,134 +1,171 @@
-from odoo.tests import Form, common
+from odoo.models import BaseModel
+from odoo.tests import Form, SavepointCase
-class TestMrpAttachmentMgmtBase(common.SavepointCase):
+class TestMrpBomAttributeMatchBase(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls._create_products(cls)
- cls._create_boms(cls)
-
- def _create_products(self):
- self.warehouse = self.env.ref("stock.warehouse0")
- route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
- self.product_sword = self.env["product.template"].create(
+ cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
+ cls.warehouse = cls.env.ref("stock.warehouse0")
+ cls.route_manufacture = cls.warehouse.manufacture_pull_id.route_id
+ # Create products
+ cls.product_sword = cls.env["product.template"].create(
{
"name": "Plastic Sword",
"type": "product",
}
)
- self.product_surf = self.env["product.template"].create(
+ cls.product_surf = cls.env["product.template"].create(
{
"name": "Surf",
"type": "product",
}
)
- self.product_fin = self.env["product.template"].create(
+ cls.product_fin = cls.env["product.template"].create(
{
"name": "Surf Fin",
"type": "product",
}
)
- self.product_plastic = self.env["product.template"].create(
+ cls.product_plastic = cls.env["product.template"].create(
{
"name": "Plastic Component",
"type": "product",
}
)
- self.p1 = self.env["product.template"].create(
+ cls.p1 = cls.env["product.template"].create(
{
"name": "P1",
"type": "product",
- "route_ids": [(6, 0, [route_manufacture])],
+ "route_ids": [(6, 0, cls.route_manufacture.ids)],
}
)
- self.p2 = self.env["product.template"].create(
+ cls.p2 = cls.env["product.template"].create(
{
"name": "P2",
"type": "product",
- "route_ids": [(6, 0, [route_manufacture])],
+ "route_ids": [(6, 0, cls.route_manufacture.ids)],
}
)
- self.p3 = self.env["product.template"].create(
+ cls.p3 = cls.env["product.template"].create(
{
"name": "P3",
"type": "product",
- "route_ids": [(6, 0, [route_manufacture])],
+ "route_ids": [(6, 0, cls.route_manufacture.ids)],
}
)
- self.product_9 = self.env["product.product"].create(
+ cls.product_9 = cls.env["product.product"].create(
{
"name": "Paper",
}
)
- self.product_10 = self.env["product.product"].create(
+ cls.product_10 = cls.env["product.product"].create(
{
"name": "Stone",
}
)
- self.product_attribute = self.env["product.attribute"].create(
+ cls.product_attribute = cls.env["product.attribute"].create(
{"name": "Colour", "display_type": "radio", "create_variant": "always"}
)
- self.attribute_value_ids = self.env["product.attribute.value"].create(
+ cls.attribute_value_ids = cls.env["product.attribute.value"].create(
[
- {"name": "Cyan", "attribute_id": self.product_attribute.id},
- {"name": "Magenta", "attribute_id": self.product_attribute.id},
+ {"name": "Cyan", "attribute_id": cls.product_attribute.id},
+ {"name": "Magenta", "attribute_id": cls.product_attribute.id},
]
)
- self.plastic_attrs = self.env["product.template.attribute.line"].create(
+ cls.plastic_attrs = cls.env["product.template.attribute.line"].create(
{
- "attribute_id": self.product_attribute.id,
- "product_tmpl_id": self.product_plastic.id,
- "value_ids": [(6, 0, self.product_attribute.value_ids.ids)],
+ "attribute_id": cls.product_attribute.id,
+ "product_tmpl_id": cls.product_plastic.id,
+ "value_ids": [(6, 0, cls.product_attribute.value_ids.ids)],
}
)
- self.sword_attrs = self.env["product.template.attribute.line"].create(
+ cls.sword_attrs = cls.env["product.template.attribute.line"].create(
{
- "attribute_id": self.product_attribute.id,
- "product_tmpl_id": self.product_sword.id,
- "value_ids": [(6, 0, self.product_attribute.value_ids.ids)],
+ "attribute_id": cls.product_attribute.id,
+ "product_tmpl_id": cls.product_sword.id,
+ "value_ids": [(6, 0, cls.product_attribute.value_ids.ids)],
}
)
+ # Create boms
+ cls.bom_id = cls._create_bom(
+ cls.product_sword,
+ [
+ dict(
+ component_template_id=cls.product_plastic.id,
+ product_qty=1,
+ ),
+ dict(
+ product_id=cls.product_9,
+ product_qty=1,
+ ),
+ ],
+ )
+ cls.fin_bom_id = cls._create_bom(
+ cls.product_fin,
+ [
+ dict(
+ product_id=cls.product_plastic.product_variant_ids[0],
+ product_qty=1,
+ ),
+ ],
+ )
+ cls.surf_bom_id = cls._create_bom(
+ cls.product_surf,
+ [
+ dict(
+ product_id=cls.product_fin.product_variant_ids[0],
+ product_qty=1,
+ ),
+ ],
+ )
+ cls.p1_bom_id = cls._create_bom(
+ cls.p1,
+ [
+ dict(
+ product_id=cls.p2.product_variant_ids[0],
+ product_qty=1,
+ ),
+ ],
+ )
+ cls.p2_bom_id = cls._create_bom(
+ cls.p2,
+ [
+ dict(
+ product_id=cls.p3.product_variant_ids[0],
+ product_qty=1,
+ ),
+ ],
+ )
+ cls.p3_bom_id = cls._create_bom(
+ cls.p3,
+ [
+ dict(
+ product_id=cls.p1.product_variant_ids[0],
+ product_qty=1,
+ ),
+ ],
+ )
- def _create_boms(self):
- mrp_bom_form = Form(self.env["mrp.bom"])
- mrp_bom_form.product_tmpl_id = self.product_sword
- with mrp_bom_form.bom_line_ids.new() as line_form:
- line_form.component_template_id = self.product_plastic
- line_form.product_qty = 1
- self.bom_id = mrp_bom_form.save()
-
- mrp_bom_form = Form(self.env["mrp.bom"])
- mrp_bom_form.product_tmpl_id = self.product_fin
- with mrp_bom_form.bom_line_ids.new() as line_form:
- line_form.product_id = self.product_plastic.product_variant_ids[0]
- line_form.product_qty = 1
- self.fin_bom_id = mrp_bom_form.save()
-
- mrp_bom_form = Form(self.env["mrp.bom"])
- mrp_bom_form.product_tmpl_id = self.product_surf
- with mrp_bom_form.bom_line_ids.new() as line_form:
- line_form.product_id = self.product_fin.product_variant_ids[0]
- line_form.product_qty = 1
- self.surf_bom_id = mrp_bom_form.save()
-
- mrp_bom_form = Form(self.env["mrp.bom"])
- mrp_bom_form.product_tmpl_id = self.p1
- with mrp_bom_form.bom_line_ids.new() as line_form:
- line_form.product_id = self.p2.product_variant_ids[0]
- line_form.product_qty = 1
- self.p1_bom_id = mrp_bom_form.save()
-
- mrp_bom_form = Form(self.env["mrp.bom"])
- mrp_bom_form.product_tmpl_id = self.p2
- with mrp_bom_form.bom_line_ids.new() as line_form:
- line_form.product_id = self.p3.product_variant_ids[0]
- line_form.product_qty = 1
- self.p2_bom_id = mrp_bom_form.save()
-
- mrp_bom_form = Form(self.env["mrp.bom"])
- mrp_bom_form.product_tmpl_id = self.p3
- with mrp_bom_form.bom_line_ids.new() as line_form:
- line_form.product_id = self.p1.product_variant_ids[0]
- line_form.product_qty = 1
- self.p3_bom_id = mrp_bom_form.save()
+ @classmethod
+ def _create_bom(cls, product, line_form_vals):
+ if product._name == "product.template":
+ template = product
+ product = cls.env["product.product"]
+ else:
+ template = product.product_tmpl_id
+ with Form(cls.env["mrp.bom"]) as form:
+ form.product_tmpl_id = template
+ form.product_id = product
+ for vals in line_form_vals:
+ with form.bom_line_ids.new() as line_form:
+ for key, value in vals.items():
+ field = line_form._model._fields.get(key)
+ if field and field.relational: # pragma: no cover
+ if value and not isinstance(value, BaseModel):
+ value = cls.env[field.comodel_name].browse(value)
+ elif not value:
+ value = cls.env[field.comodel_name]
+ setattr(line_form, key, value)
+ return form.save()
diff --git a/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match.py b/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match.py
index 62a30adb1..141881e82 100644
--- a/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match.py
+++ b/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match.py
@@ -1,10 +1,10 @@
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form
-from .common import TestMrpAttachmentMgmtBase
+from .common import TestMrpBomAttributeMatchBase
-class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
+class TestMrpAttachmentMgmt(TestMrpBomAttributeMatchBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -67,7 +67,7 @@ class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
self.mo_sword.action_confirm()
# Assert correct component variant was selected automatically
self.assertEqual(
- self.mo_sword.move_raw_ids.product_id.display_name,
+ self.mo_sword.move_raw_ids.product_id[0].display_name,
"Plastic Component (Cyan)",
)
@@ -81,9 +81,8 @@ class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
mo_form.bom_id = self.bom_id
mo_form.product_qty = 1
self.mo_sword = mo_form.save()
- with self.assertRaises(UserError):
- # Add some materials to consume before marking this MO as to do.
- self.mo_sword.action_confirm()
+ # Add some materials to consume before marking this MO as to do.
+ self.mo_sword.action_confirm()
def test_manufacturing_order_3(self):
# Delete attribute from sword
diff --git a/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match_nested.py b/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match_nested.py
new file mode 100644
index 000000000..7865fb5d4
--- /dev/null
+++ b/mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match_nested.py
@@ -0,0 +1,87 @@
+from .common import TestMrpBomAttributeMatchBase
+
+
+class TestMrpBomAttributeMatchNested(TestMrpBomAttributeMatchBase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
+
+ def setUp(self):
+ super().setUp()
+ attr1 = self.env["product.attribute"].create(
+ {"name": "style", "display_type": "radio", "create_variant": "always"}
+ )
+ a1vs = self.env["product.attribute.value"].create(
+ [
+ {"name": "office", "attribute_id": attr1.id},
+ {"name": "gaming", "attribute_id": attr1.id},
+ ]
+ )
+
+ top = self.env["product.template"].create(
+ {
+ "name": "Top-Level",
+ "type": "product",
+ }
+ )
+ self.env["product.template.attribute.line"].create(
+ {
+ "attribute_id": attr1.id,
+ "product_tmpl_id": top.id,
+ "value_ids": [(6, 0, a1vs.ids)],
+ }
+ )
+
+ sub = self.env["product.template"].create(
+ {
+ "name": "Sub-Level",
+ "type": "product",
+ }
+ )
+ self.env["product.template.attribute.line"].create(
+ {
+ "attribute_id": attr1.id,
+ "product_tmpl_id": sub.id,
+ "value_ids": [(6, 0, a1vs.ids)],
+ }
+ )
+
+ subsub = self.env["product.template"].create(
+ {
+ "name": "Sub Sub 1",
+ "type": "product",
+ }
+ )
+ self.env["product.template.attribute.line"].create(
+ {
+ "attribute_id": attr1.id,
+ "product_tmpl_id": subsub.id,
+ "value_ids": [(6, 0, a1vs.ids)],
+ }
+ )
+
+ self.bom_sub = self._create_bom(
+ sub,
+ [
+ dict(
+ component_template_id=subsub.id,
+ product_qty=1,
+ ),
+ ],
+ )
+ self.bom_top = self._create_bom(
+ top,
+ [
+ dict(
+ component_template_id=sub.id,
+ product_qty=1,
+ ),
+ ],
+ )
+
+ def test_nested(self):
+ BomStructureReport = self.env["report.mrp.report_bom_structure"]
+
+ BomStructureReport._get_report_data(self.bom_sub.id)
+ BomStructureReport._get_report_data(self.bom_top.id)