[IMP] mrp_bom_attribute_match: increase test strength: add another component to bom

This commit is contained in:
Ivàn Todorovich
2023-01-31 14:06:04 -03:00
committed by Ilyas
parent d830745fa1
commit 1022b14cea
19 changed files with 483 additions and 282 deletions

View File

@@ -1 +1,83 @@
TO BE GENERATED
===============================
MRP Account BOM Attribute Match
===============================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github
:target: https://github.com/OCA/manufacture/tree/15.0/mrp_account_bom_attribute_match
:alt: OCA/manufacture
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/manufacture-15-0/manufacture-15-0-mrp_account_bom_attribute_match
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/manufacture&target_branch=15.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
Glue module between `mrp_bom_attribute_match` and `mrp_account`.
**Table of contents**
.. contents::
:local:
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/manufacture/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_account_bom_attribute_match%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* `Camptocamp <https://www.camptocamp.com>`_
* Iván Todorovich <ivan.todorovich@camptocamp.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-ivantodorovich| image:: https://github.com/ivantodorovich.png?size=40px
:target: https://github.com/ivantodorovich
:alt: ivantodorovich
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-ivantodorovich|
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/15.0/mrp_account_bom_attribute_match>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -5,7 +5,7 @@
{
"name": "MRP Account BOM Attribute Match",
"summary": "Glue module between `mrp_account` and `mrp_bom_attribute_match`",
"version": "15.0.1.0.0",
"version": "14.0.1.0.0",
"author": "Camptocamp, Odoo Community Association (OCA)",
"maintainers": ["ivantodorovich"],
"website": "https://github.com/OCA/manufacture",

View File

@@ -2,13 +2,13 @@
# @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
from odoo import models
class ProductProduct(models.Model):
_inherit = "product.product"
def _compute_bom_price(self, bom, boms_to_recompute=False, byproduct_bom=False):
def _compute_bom_price(self, bom, boms_to_recompute=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.
@@ -30,5 +30,6 @@ class ProductProduct(models.Model):
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)
for to_ignore_line_id in to_ignore_line_ids:
bom.bom_line_ids = [(3, to_ignore_line_id, 0)]
return super()._compute_bom_price(bom, boms_to_recompute)

View File

@@ -0,0 +1,8 @@
* `Camptocamp <https://www.camptocamp.com>`_
* Iván Todorovich <ivan.todorovich@camptocamp.com>
* `Ooops404 <https://ooops404.com>`_
* Ilyas <irazor147@gmail.com>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -1 +1,2 @@
from . import common
from . import test_mrp_account_bom_attribute_match

View File

@@ -0,0 +1,171 @@
from odoo.models import BaseModel
from odoo.tests import Form, SavepointCase
class TestMrpBomAttributeMatchBase(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
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",
}
)
cls.product_surf = cls.env["product.template"].create(
{
"name": "Surf",
"type": "product",
}
)
cls.product_fin = cls.env["product.template"].create(
{
"name": "Surf Fin",
"type": "product",
}
)
cls.product_plastic = cls.env["product.template"].create(
{
"name": "Plastic Component",
"type": "product",
}
)
cls.p1 = cls.env["product.template"].create(
{
"name": "P1",
"type": "product",
"route_ids": [(4, cls.route_manufacture.id, 0)],
}
)
cls.p2 = cls.env["product.template"].create(
{
"name": "P2",
"type": "product",
"route_ids": [(4, cls.route_manufacture.id, 0)],
}
)
cls.p3 = cls.env["product.template"].create(
{
"name": "P3",
"type": "product",
"route_ids": [(4, cls.route_manufacture.id, 0)],
}
)
cls.product_9 = cls.env["product.product"].create(
{
"name": "Paper",
}
)
cls.product_10 = cls.env["product.product"].create(
{
"name": "Stone",
}
)
cls.product_attribute = cls.env["product.attribute"].create(
{"name": "Colour", "display_type": "radio", "create_variant": "always"}
)
cls.attribute_value_ids = cls.env["product.attribute.value"].create(
[
{"name": "Cyan", "attribute_id": cls.product_attribute.id},
{"name": "Magenta", "attribute_id": cls.product_attribute.id},
]
)
cls.plastic_attrs = cls.env["product.template.attribute.line"].create(
{
"attribute_id": cls.product_attribute.id,
"product_tmpl_id": cls.product_plastic.id,
"value_ids": [(6, 0, cls.product_attribute.value_ids.ids)],
}
)
cls.sword_attrs = cls.env["product.template.attribute.line"].create(
{
"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,
),
],
)
@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

@@ -2,9 +2,7 @@
# @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,
)
from .common import TestMrpBomAttributeMatchBase
class TestMrpAccount(TestMrpBomAttributeMatchBase):

View File

@@ -10,7 +10,4 @@
"data": [
"views/mrp_bom_views.xml",
],
"qweb": [],
"application": False,
"installable": True,
}

View File

@@ -18,7 +18,10 @@ class MrpBomLine(models.Model):
"product.template", "Component (product template)"
)
match_on_attribute_ids = fields.Many2many(
"product.attribute", string="Match on Attributes", readonly=True
"product.attribute",
string="Match on Attributes",
compute="_compute_match_on_attribute_ids",
store=True,
)
product_uom_category_id = fields.Many2one(
"uom.category",
@@ -56,87 +59,93 @@ class MrpBomLine(models.Model):
@api.onchange("component_template_id")
def _onchange_component_template_id(self):
if self.component_template_id:
if self.product_id:
self.product_backup_id = self.product_id
self.product_id = False
if (
self.product_uom_id.category_id
!= self.component_template_id.uom_id.category_id
):
self.product_uom_id = self.component_template_id.uom_id
else:
if self.product_backup_id:
self.product_id = self.product_backup_id
self.product_backup_id = False
if self.product_uom_id.category_id != self.product_id.uom_id.category_id:
self.product_uom_id = self.product_id.uom_id
self._update_component_attributes()
def _update_component_attributes(self):
if self.component_template_id:
self._check_component_attributes()
self.product_backup_id = self.product_id.id
self.match_on_attribute_ids = (
self.component_template_id.attribute_line_ids.mapped("attribute_id")
.filtered(lambda x: x.create_variant != "no_variant")
.ids
)
self.product_id = False
self._check_variants_validity()
else:
self.match_on_attribute_ids = False
if self.product_backup_id and not self.product_id:
self.product_id = self.product_backup_id.id
self.product_backup_id = False
@api.depends("component_template_id")
def _compute_match_on_attribute_ids(self):
for rec in self:
if rec.component_template_id:
rec.match_on_attribute_ids = (
rec.component_template_id.attribute_line_ids.attribute_id.filtered(
lambda x: x.create_variant != "no_variant"
)
)
else:
rec.match_on_attribute_ids = False
@api.constrains("component_template_id")
def _check_component_attributes(self):
comp_attr_ids = (
self.component_template_id.valid_product_template_attribute_line_ids.attribute_id.ids
)
prod_attr_ids = (
self.bom_id.product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id.ids
)
if len(comp_attr_ids) == 0:
raise ValidationError(
_(
"No match on attribute has been detected for Component "
"(Product Template) %s",
self.component_template_id.display_name,
)
for rec in self:
if not rec.component_template_id:
continue
comp_attrs = (
rec.component_template_id.valid_product_template_attribute_line_ids.attribute_id
)
if not all(item in prod_attr_ids for item in comp_attr_ids):
raise ValidationError(
_(
"Some attributes of the dynamic component are not included into "
"production product attributes."
)
prod_attrs = (
rec.bom_id.product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id
)
if not comp_attrs:
raise ValidationError(
_(
"No match on attribute has been detected for Component "
"(Product Template) %s",
rec.component_template_id.display_name,
)
)
if not all(attr in prod_attrs for attr in comp_attrs):
raise ValidationError(
_(
"Some attributes of the dynamic component are not included into "
"production product attributes."
)
)
@api.constrains("component_template_id", "bom_product_template_attribute_value_ids")
def _check_variants_validity(self):
for rec in self:
if (
not rec.bom_product_template_attribute_value_ids
or not rec.component_template_id
):
continue
variant_attrs = rec.bom_product_template_attribute_value_ids.attribute_id
same_attr_ids = set(rec.match_on_attribute_ids.ids) & set(variant_attrs.ids)
same_attrs = self.env["product.attribute"].browse(same_attr_ids)
if same_attrs:
raise ValidationError(
_(
"You cannot use an attribute value for attribute(s) %(attributes)s "
"in the field “Apply on Variants” as it's the same attribute used "
"in the field “Match on Attribute” related to the component "
"%(component)s.",
attributes=", ".join(same_attrs.mapped("name")),
component=rec.component_template_id.name,
)
)
@api.onchange("match_on_attribute_ids")
def _onchange_match_on_attribute_ids_check_component_attributes(self):
if self.match_on_attribute_ids:
self._check_component_attributes()
@api.onchange("bom_product_template_attribute_value_ids")
def _onchange_attribute_value_ids(self):
def _onchange_bom_product_template_attribute_value_ids_check_variants(self):
if self.bom_product_template_attribute_value_ids:
self._check_variants_validity()
def _check_variants_validity(self):
if (
not self.bom_product_template_attribute_value_ids
or not self.component_template_id
):
return
variant_attr_ids = self.bom_product_template_attribute_value_ids.mapped(
"attribute_id"
)
same_attrs = set(self.match_on_attribute_ids.ids) & set(variant_attr_ids.ids)
if len(same_attrs) > 0:
attr_recs = self.env["product.attribute"].browse(same_attrs)
raise ValidationError(
_(
"You cannot use an attribute value for attribute(s) %(attributes)s "
"in the field “Apply on Variants” as it's the same attribute used "
"in the field “Match on Attribute” related to the component "
"%(component)s.",
attributes=", ".join(attr_recs.mapped("name")),
component=self.component_template_id.name,
)
)
def write(self, vals):
super(MrpBomLine, self).write(vals)
class MrpBom(models.Model):
_inherit = "mrp.bom"
@@ -225,8 +234,7 @@ class MrpBom(models.Model):
)
if component_template_product:
# need to set product_id temporary
if current_line.product_id != component_template_product:
current_line.product_id = component_template_product
current_line.product_id = component_template_product
else:
# component_template_id is set, but no attribute value match.
continue
@@ -337,8 +345,10 @@ class MrpBom(models.Model):
else:
return line_product_id
def write(self, vals):
res = super(MrpBom, self).write(vals)
for line in self.bom_line_ids:
line._update_component_attributes()
return res
@api.constrains("product_tmpl_id", "product_id")
def _check_component_attributes(self):
return self.bom_line_ids._check_component_attributes()
@api.constrains("product_tmpl_id", "product_id")
def _check_variants_validity(self):
return self.bom_line_ids._check_variants_validity()

View File

@@ -1,4 +1,4 @@
from odoo import models
from odoo import api, models
class MrpProduction(models.Model):
@@ -12,7 +12,6 @@ class MrpProduction(models.Model):
bom_line.product_id = False
return res
def write(self, vals):
for bl in self.bom_id.bom_line_ids.filtered("component_template_id"):
bl._check_component_attributes()
return super().write(vals)
@api.constrains("bom_id")
def _check_component_attributes(self):
self.bom_id._check_component_attributes()

View File

@@ -1,31 +1,24 @@
from odoo import _, models
from odoo import _, api, models
from odoo.exceptions import UserError
class ProductTemplate(models.Model):
_inherit = "product.template"
def write(self, vals):
res = super().write(vals)
for rec in self:
rec._check_product_with_component_change_allowed()
rec._check_component_change_allowed()
return res
@api.constrains("attribute_line_ids")
def _check_product_with_component_change_allowed(self):
self.ensure_one()
if len(self.attribute_line_ids) > 0 and len(self.bom_ids) > 0:
for bom in self.bom_ids:
for line in bom.bom_line_ids.filtered(
lambda x: x.match_on_attribute_ids
):
prod_attr_ids = self.attribute_line_ids.attribute_id.filtered(
for rec in self:
if not rec.attribute_line_ids:
continue
for bom in rec.bom_ids:
for line in bom.bom_line_ids.filtered("match_on_attribute_ids"):
prod_attr_ids = rec.attribute_line_ids.attribute_id.filtered(
lambda x: x.create_variant != "no_variant"
).ids
comp_attr_ids = line.match_on_attribute_ids.ids
diff = list(set(comp_attr_ids) - set(prod_attr_ids))
if len(diff) > 0:
attr_recs = self.env["product.attribute"].browse(diff)
diff_ids = list(set(comp_attr_ids) - set(prod_attr_ids))
diff = rec.env["product.attribute"].browse(diff_ids)
if diff:
raise UserError(
_(
"The attributes you're trying to remove are used in "
@@ -33,33 +26,36 @@ class ProductTemplate(models.Model):
"To remove these attributes, first remove the BOM line "
"with the matching component.\n"
"Attributes: %(attributes)s\nBoM: %(bom)s",
attributes=", ".join(attr_recs.mapped("name")),
attributes=", ".join(diff.mapped("name")),
bom=bom.display_name,
)
)
@api.constrains("attribute_line_ids")
def _check_component_change_allowed(self):
self.ensure_one()
if len(self.attribute_line_ids) > 0:
for rec in self:
if not rec.attribute_line_ids:
continue
boms = self._get_component_boms()
if boms:
for bom in boms:
vpa = bom.product_tmpl_id.valid_product_template_attribute_line_ids
prod_attr_ids = vpa.attribute_id.ids
comp_attr_ids = self.attribute_line_ids.attribute_id.ids
diff = list(set(comp_attr_ids) - set(prod_attr_ids))
if len(diff) > 0:
attr_recs = self.env["product.attribute"].browse(diff)
raise UserError(
_(
"This product template is used as a component in the "
"BOMs for %(bom)s and attribute(s) %(attributes)s is "
"not present in all such product(s), and this would "
"break the BOM behavior.",
attributes=", ".join(attr_recs.mapped("name")),
bom=bom.display_name,
)
if not boms:
continue
for bom in boms:
vpa = bom.product_tmpl_id.valid_product_template_attribute_line_ids
prod_attr_ids = vpa.attribute_id.ids
comp_attr_ids = self.attribute_line_ids.attribute_id.ids
diff = list(set(comp_attr_ids) - set(prod_attr_ids))
if len(diff) > 0:
attr_recs = self.env["product.attribute"].browse(diff)
raise UserError(
_(
"This product template is used as a component in the "
"BOMs for %(bom)s and attribute(s) %(attributes)s is "
"not present in all such product(s), and this would "
"break the BOM behavior.",
attributes=", ".join(attr_recs.mapped("name")),
bom=bom.display_name,
)
)
def _get_component_boms(self):
self.ensure_one()

View File

@@ -1,3 +1,7 @@
* Ooops404 <https://ooops404.com>
* Ilyas
* `Camptocamp <https://www.camptocamp.com>`_
* Iván Todorovich <ivan.todorovich@camptocamp.com>

View File

@@ -2,7 +2,7 @@
# @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
from odoo import models
class ReportBomStructure(models.AbstractModel):
@@ -30,7 +30,8 @@ class ReportBomStructure(models.AbstractModel):
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]
for to_ignore_line_id in to_ignore_line_ids:
bom.bom_line_ids = [(3, to_ignore_line_id, 0)]
components, total = super()._get_bom_lines(
bom, bom_quantity, product, line_id, level
)

View File

@@ -1,168 +1,134 @@
from odoo import Command
from odoo.models import BaseModel
from odoo.tests import Form, TransactionCase
from odoo.tests import Form, common
class TestMrpBomAttributeMatchBase(TransactionCase):
class TestMrpAttachmentMgmtBase(common.SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
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(
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(
{
"name": "Plastic Sword",
"type": "product",
}
)
cls.product_surf = cls.env["product.template"].create(
self.product_surf = self.env["product.template"].create(
{
"name": "Surf",
"type": "product",
}
)
cls.product_fin = cls.env["product.template"].create(
self.product_fin = self.env["product.template"].create(
{
"name": "Surf Fin",
"type": "product",
}
)
cls.product_plastic = cls.env["product.template"].create(
self.product_plastic = self.env["product.template"].create(
{
"name": "Plastic Component",
"type": "product",
}
)
cls.p1 = cls.env["product.template"].create(
self.p1 = self.env["product.template"].create(
{
"name": "P1",
"type": "product",
"route_ids": [Command.link(cls.route_manufacture.id)],
"route_ids": [(6, 0, [route_manufacture])],
}
)
cls.p2 = cls.env["product.template"].create(
self.p2 = self.env["product.template"].create(
{
"name": "P2",
"type": "product",
"route_ids": [Command.link(cls.route_manufacture.id)],
"route_ids": [(6, 0, [route_manufacture])],
}
)
cls.p3 = cls.env["product.template"].create(
self.p3 = self.env["product.template"].create(
{
"name": "P3",
"type": "product",
"route_ids": [Command.link(cls.route_manufacture.id)],
"route_ids": [(6, 0, [route_manufacture])],
}
)
cls.product_9 = cls.env["product.product"].create(
self.product_9 = self.env["product.product"].create(
{
"name": "Paper",
}
)
cls.product_10 = cls.env["product.product"].create(
self.product_10 = self.env["product.product"].create(
{
"name": "Stone",
}
)
cls.product_attribute = cls.env["product.attribute"].create(
self.product_attribute = self.env["product.attribute"].create(
{"name": "Colour", "display_type": "radio", "create_variant": "always"}
)
cls.attribute_value_ids = cls.env["product.attribute.value"].create(
self.attribute_value_ids = self.env["product.attribute.value"].create(
[
{"name": "Cyan", "attribute_id": cls.product_attribute.id},
{"name": "Magenta", "attribute_id": cls.product_attribute.id},
{"name": "Cyan", "attribute_id": self.product_attribute.id},
{"name": "Magenta", "attribute_id": self.product_attribute.id},
]
)
cls.plastic_attrs = cls.env["product.template.attribute.line"].create(
self.plastic_attrs = self.env["product.template.attribute.line"].create(
{
"attribute_id": cls.product_attribute.id,
"product_tmpl_id": cls.product_plastic.id,
"value_ids": [Command.set(cls.product_attribute.value_ids.ids)],
"attribute_id": self.product_attribute.id,
"product_tmpl_id": self.product_plastic.id,
"value_ids": [(6, 0, self.product_attribute.value_ids.ids)],
}
)
cls.sword_attrs = cls.env["product.template.attribute.line"].create(
self.sword_attrs = self.env["product.template.attribute.line"].create(
{
"attribute_id": cls.product_attribute.id,
"product_tmpl_id": cls.product_sword.id,
"value_ids": [Command.set(cls.product_attribute.value_ids.ids)],
"attribute_id": self.product_attribute.id,
"product_tmpl_id": self.product_sword.id,
"value_ids": [(6, 0, self.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,
),
],
)
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,
),
],
)
@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()
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()

View File

@@ -1,10 +1,14 @@
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form
from .common import TestMrpBomAttributeMatchBase
from .common import TestMrpAttachmentMgmtBase
class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
class TestMrpAttachmentMgmt(TestMrpAttachmentMgmtBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
def test_bom_1(self):
mrp_bom_form = Form(self.env["mrp.bom"])
mrp_bom_form.product_tmpl_id = self.product_sword
@@ -19,12 +23,7 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
line_form.component_template_id = self.product_plastic
line_form.product_qty = 1
sword_cyan = self.sword_attrs.product_template_value_ids[0]
with self.assertRaisesRegex(
ValidationError,
r"You cannot use an attribute value for attribute\(s\) .* in the "
r"field “Apply on Variants” as it's the same attribute used in the "
r"field “Match on Attribute” related to the component .*",
):
with self.assertRaises(ValidationError):
line_form.bom_product_template_attribute_value_ids.add(sword_cyan)
def test_bom_2(self):
@@ -43,12 +42,7 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
"value_ids": [(4, orchid_attribute_value_id.id)],
}
)
with self.assertRaisesRegex(
UserError,
r"This product template is used as a component in the BOMs for .* and "
r"attribute\(s\) .* is not present in all such product\(s\), and this "
r"would break the BOM behavior\.",
):
with self.assertRaises(UserError):
vals = {
"attribute_id": smell_attribute.id,
"product_tmpl_id": self.product_plastic.id,
@@ -58,11 +52,7 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
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:
with self.assertRaisesRegex(
UserError,
r"Some attributes of the dynamic component are not included into "
r"production product attributes\.",
):
with self.assertRaises(ValidationError):
line_form.component_template_id = self.product_plastic
plastic_smells_like_orchid.unlink()
@@ -103,10 +93,8 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
# Component skipped
mo_form.bom_id = self.bom_id
mo_form.product_qty = 1
with self.assertRaisesRegex(
ValidationError,
r"Some attributes of the dynamic component are not included into .+",
):
with self.assertRaises(ValidationError):
# Some attributes of the dynamic component are not included into ...
self.mo_sword = mo_form.save()
def test_manufacturing_order_4(self):
@@ -168,20 +156,5 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
"product_qty": 1.0,
}
)
with self.assertRaisesRegex(UserError, r"Recursion error! .+"):
with self.assertRaises(UserError):
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.id,
)
self.assertEqual(
res["lines"]["components"][0]["parent_id"],
self.bom_id.id,
)

View File

@@ -1,17 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="mrp_bom_view_form_inherit_bom_match" model="ir.ui.view">
<field name="name">mrp.bom.view.form.inherit.bom.match</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
<field name="arch" type="xml">
<field name="bom_product_template_attribute_value_ids" position="before">
<field
name="match_on_attribute_ids"
readonly='1'
force_save="1"
widget="many2many_tags"
/>
<field name="match_on_attribute_ids" widget="many2many_tags" />
<field name="product_backup_id" invisible="1" />
</field>
<xpath