[FIX] mrp_bom_attribute_match: BOM Structure and cost nested

This commit is contained in:
Ilyas
2023-10-04 16:19:35 +02:00
parent 035a4de34a
commit fe864284d7
5 changed files with 270 additions and 81 deletions

View File

@@ -1,8 +1,8 @@
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# 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,7 @@ 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 +44,67 @@ 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
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
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

View File

@@ -1 +1,2 @@
from . import test_mrp_bom_attribute_match
from . import test_mrp_bom_attribute_match_nested

View File

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

View File

@@ -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

View File

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