Merge PR #958 into 15.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2023-02-14 12:55:37 +00:00
14 changed files with 267 additions and 84 deletions

View File

@@ -0,0 +1 @@
TO BE GENERATED

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,16 @@
# 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).
{
"name": "MRP Account BOM Attribute Match",
"summary": "Glue module between `mrp_account` and `mrp_bom_attribute_match`",
"version": "15.0.1.0.0",
"author": "Camptocamp, Odoo Community Association (OCA)",
"maintainers": ["ivantodorovich"],
"website": "https://github.com/OCA/manufacture",
"license": "AGPL-3",
"category": "Manufacturing",
"depends": ["mrp_account", "mrp_bom_attribute_match"],
"auto_install": True,
}

View File

@@ -0,0 +1 @@
from . import product_product

View File

@@ -0,0 +1,34 @@
# 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 Command, models
class ProductProduct(models.Model):
_inherit = "product.product"
def _compute_bom_price(self, bom, boms_to_recompute=False, byproduct_bom=False):
# OVERRIDE to fill in the `line.product_id` if a component template is used.
# To avoid a complete override, we HACK the bom by replacing it with a virtual
# record, and modifying it's lines on-the-fly.
has_template_lines = bom and any(
line.component_template_id for line in bom.bom_line_ids
)
if has_template_lines:
bom = bom.new(origin=bom)
to_ignore_line_ids = []
for line in bom.bom_line_ids:
if line._skip_bom_line(self) or not line.component_template_id:
continue
line_product = bom._get_component_template_product(
line, self, line.product_id
)
if not line_product:
to_ignore_line_ids.append(line.id)
continue
else:
line.product_id = line_product
if to_ignore_line_ids:
bom.bom_line_ids = [Command.unlink(id) for id in to_ignore_line_ids]
return super()._compute_bom_price(bom, boms_to_recompute, byproduct_bom)

View File

@@ -0,0 +1 @@
from . import test_mrp_account_bom_attribute_match

View File

@@ -0,0 +1,23 @@
# 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.addons.mrp_bom_attribute_match.tests.common import (
TestMrpBomAttributeMatchBase,
)
class TestMrpAccount(TestMrpBomAttributeMatchBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
def test_bom_cost(self):
sword_cyan, sword_magenta = self.product_sword.product_variant_ids
plastic_cyan, plastic_magenta = self.product_plastic.product_variant_ids
plastic_cyan.standard_price = 1.00
plastic_magenta.standard_price = 2.00
sword_cyan.button_bom_cost()
sword_magenta.button_bom_cost()
self.assertEqual(sword_cyan.standard_price, 1.00)
self.assertEqual(sword_magenta.standard_price, 2.00)

View File

@@ -1 +1,2 @@
from . import models
from . import reports

View File

@@ -0,0 +1 @@
from . import mrp_report_bom_structure

View File

@@ -0,0 +1,44 @@
# 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 Command, models
class ReportBomStructure(models.AbstractModel):
_inherit = "report.mrp.report_bom_structure"
def _get_bom_lines(self, bom, bom_quantity, product, line_id, level):
# OVERRIDE to fill in the `line.product_id` if a component template is used.
# To avoid a complete override, we HACK the bom by replacing it with a virtual
# record, and modifying it's lines on-the-fly.
has_template_lines = any(
line.component_template_id for line in bom.bom_line_ids
)
if has_template_lines:
bom = bom.new(origin=bom)
to_ignore_line_ids = []
for line in bom.bom_line_ids:
if line._skip_bom_line(product) or not line.component_template_id:
continue
line_product = bom._get_component_template_product(
line, product, line.product_id
)
if not line_product:
to_ignore_line_ids.append(line.id)
continue
else:
line.product_id = line_product
if to_ignore_line_ids:
bom.bom_line_ids = [Command.unlink(id) for id in to_ignore_line_ids]
components, total = super()._get_bom_lines(
bom, bom_quantity, product, line_id, level
)
# Replace any NewId value by the real record id
# Otherwise it's evaluated as False in some situations, and it may cause issues
if has_template_lines:
for component in components:
for key, value in component.items():
if isinstance(value, models.NewId):
component[key] = value.origin
return components, total

View File

@@ -1,134 +1,172 @@
from odoo import Command
from odoo.models import BaseModel
from odoo.tests import Form, TransactionCase
class TestMrpAttachmentMgmtBase(TransactionCase):
class TestMrpBomAttributeMatchBase(TransactionCase):
@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": [Command.link(cls.route_manufacture.id)],
}
)
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": [Command.link(cls.route_manufacture.id)],
}
)
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": [Command.link(cls.route_manufacture.id)],
}
)
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": [Command.set(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": [Command.set(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,14 +1,10 @@
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form
from .common import TestMrpAttachmentMgmtBase
from .common import TestMrpBomAttributeMatchBase
class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
def test_bom_1(self):
mrp_bom_form = Form(self.env["mrp.bom"])
mrp_bom_form.product_tmpl_id = self.product_sword
@@ -71,18 +67,18 @@ class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
plastic_smells_like_orchid.unlink()
def test_manufacturing_order_1(self):
sword_cyan = self.product_sword.product_variant_ids[0]
plastic_cyan = self.product_plastic.product_variant_ids[0]
mo_form = Form(self.env["mrp.production"])
mo_form.product_id = self.product_sword.product_variant_ids.filtered(
lambda x: x.display_name == "Plastic Sword (Cyan)"
)
mo_form.product_id = sword_cyan
mo_form.bom_id = self.bom_id
mo_form.product_qty = 1
self.mo_sword = mo_form.save()
self.mo_sword.action_confirm()
# Assert correct component variant was selected automatically
self.assertEqual(
self.mo_sword.move_raw_ids.product_id.display_name,
"Plastic Component (Cyan)",
self.mo_sword.move_raw_ids.product_id,
plastic_cyan + self.product_9,
)
def test_manufacturing_order_2(self):
@@ -172,3 +168,22 @@ class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
)
with self.assertRaisesRegex(UserError, r"Recursion error! .+"):
test_bom_3.explode(self.product_9, 1)
def test_mrp_report_bom_structure(self):
sword_cyan = self.product_sword.product_variant_ids[0]
BomStructureReport = self.env["report.mrp.report_bom_structure"]
res = BomStructureReport._get_report_data(self.bom_id.id)
self.assertTrue(res["is_variant_applied"])
self.assertEqual(res["lines"]["product"], sword_cyan)
self.assertEqual(
res["lines"]["components"][0]["line_id"],
self.bom_id.bom_line_ids[0].id,
)
self.assertEqual(
res["lines"]["components"][1]["line_id"],
self.bom_id.bom_line_ids[1].id,
)
self.assertEqual(
res["lines"]["components"][0]["parent_id"],
self.bom_id.id,
)

View File

@@ -0,0 +1 @@
../../../../mrp_account_bom_attribute_match

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)