mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
1
mrp_account_bom_attribute_match/README.rst
Normal file
1
mrp_account_bom_attribute_match/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
TO BE GENERATED
|
||||
1
mrp_account_bom_attribute_match/__init__.py
Normal file
1
mrp_account_bom_attribute_match/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
16
mrp_account_bom_attribute_match/__manifest__.py
Normal file
16
mrp_account_bom_attribute_match/__manifest__.py
Normal 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,
|
||||
}
|
||||
1
mrp_account_bom_attribute_match/models/__init__.py
Normal file
1
mrp_account_bom_attribute_match/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import product_product
|
||||
34
mrp_account_bom_attribute_match/models/product_product.py
Normal file
34
mrp_account_bom_attribute_match/models/product_product.py
Normal 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)
|
||||
1
mrp_account_bom_attribute_match/tests/__init__.py
Normal file
1
mrp_account_bom_attribute_match/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_mrp_account_bom_attribute_match
|
||||
@@ -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)
|
||||
@@ -1 +1,2 @@
|
||||
from . import models
|
||||
from . import reports
|
||||
|
||||
1
mrp_bom_attribute_match/reports/__init__.py
Normal file
1
mrp_bom_attribute_match/reports/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import mrp_report_bom_structure
|
||||
44
mrp_bom_attribute_match/reports/mrp_report_bom_structure.py
Normal file
44
mrp_bom_attribute_match/reports/mrp_report_bom_structure.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../../../mrp_account_bom_attribute_match
|
||||
6
setup/mrp_account_bom_attribute_match/setup.py
Normal file
6
setup/mrp_account_bom_attribute_match/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
Reference in New Issue
Block a user