[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", "name": "MRP Account BOM Attribute Match",
"summary": "Glue module between `mrp_account` and `mrp_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)", "author": "Camptocamp, Odoo Community Association (OCA)",
"maintainers": ["ivantodorovich"], "maintainers": ["ivantodorovich"],
"website": "https://github.com/OCA/manufacture", "website": "https://github.com/OCA/manufacture",

View File

@@ -2,13 +2,13 @@
# @author Iván Todorovich <ivan.todorovich@camptocamp.com> # @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # 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): class ProductProduct(models.Model):
_inherit = "product.product" _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. # 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 # To avoid a complete override, we HACK the bom by replacing it with a virtual
# record, and modifying it's lines on-the-fly. # record, and modifying it's lines on-the-fly.
@@ -30,5 +30,6 @@ class ProductProduct(models.Model):
else: else:
line.product_id = line_product line.product_id = line_product
if to_ignore_line_ids: 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:
return super()._compute_bom_price(bom, boms_to_recompute, byproduct_bom) 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 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> # @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.addons.mrp_bom_attribute_match.tests.common import ( from .common import TestMrpBomAttributeMatchBase
TestMrpBomAttributeMatchBase,
)
class TestMrpAccount(TestMrpBomAttributeMatchBase): class TestMrpAccount(TestMrpBomAttributeMatchBase):

View File

@@ -10,7 +10,4 @@
"data": [ "data": [
"views/mrp_bom_views.xml", "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)" "product.template", "Component (product template)"
) )
match_on_attribute_ids = fields.Many2many( 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( product_uom_category_id = fields.Many2one(
"uom.category", "uom.category",
@@ -56,87 +59,93 @@ class MrpBomLine(models.Model):
@api.onchange("component_template_id") @api.onchange("component_template_id")
def _onchange_component_template_id(self): def _onchange_component_template_id(self):
if self.component_template_id: if self.component_template_id:
if self.product_id:
self.product_backup_id = self.product_id
self.product_id = False
if ( if (
self.product_uom_id.category_id self.product_uom_id.category_id
!= self.component_template_id.uom_id.category_id != self.component_template_id.uom_id.category_id
): ):
self.product_uom_id = self.component_template_id.uom_id self.product_uom_id = self.component_template_id.uom_id
else: 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: if self.product_uom_id.category_id != self.product_id.uom_id.category_id:
self.product_uom_id = self.product_id.uom_id self.product_uom_id = self.product_id.uom_id
self._update_component_attributes()
def _update_component_attributes(self): @api.depends("component_template_id")
if self.component_template_id: def _compute_match_on_attribute_ids(self):
self._check_component_attributes() for rec in self:
self.product_backup_id = self.product_id.id if rec.component_template_id:
self.match_on_attribute_ids = ( rec.match_on_attribute_ids = (
self.component_template_id.attribute_line_ids.mapped("attribute_id") rec.component_template_id.attribute_line_ids.attribute_id.filtered(
.filtered(lambda x: x.create_variant != "no_variant") lambda x: x.create_variant != "no_variant"
.ids )
) )
self.product_id = False else:
self._check_variants_validity() rec.match_on_attribute_ids = False
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.constrains("component_template_id")
def _check_component_attributes(self): def _check_component_attributes(self):
comp_attr_ids = ( for rec in self:
self.component_template_id.valid_product_template_attribute_line_ids.attribute_id.ids if not rec.component_template_id:
) continue
prod_attr_ids = ( comp_attrs = (
self.bom_id.product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id.ids rec.component_template_id.valid_product_template_attribute_line_ids.attribute_id
)
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,
)
) )
if not all(item in prod_attr_ids for item in comp_attr_ids): prod_attrs = (
raise ValidationError( rec.bom_id.product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id
_(
"Some attributes of the dynamic component are not included into "
"production product attributes."
)
) )
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") @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: if self.bom_product_template_attribute_value_ids:
self._check_variants_validity() 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): class MrpBom(models.Model):
_inherit = "mrp.bom" _inherit = "mrp.bom"
@@ -225,8 +234,7 @@ class MrpBom(models.Model):
) )
if component_template_product: if component_template_product:
# need to set product_id temporary # 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: else:
# component_template_id is set, but no attribute value match. # component_template_id is set, but no attribute value match.
continue continue
@@ -337,8 +345,10 @@ class MrpBom(models.Model):
else: else:
return line_product_id return line_product_id
def write(self, vals): @api.constrains("product_tmpl_id", "product_id")
res = super(MrpBom, self).write(vals) def _check_component_attributes(self):
for line in self.bom_line_ids: return self.bom_line_ids._check_component_attributes()
line._update_component_attributes()
return res @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): class MrpProduction(models.Model):
@@ -12,7 +12,6 @@ class MrpProduction(models.Model):
bom_line.product_id = False bom_line.product_id = False
return res return res
def write(self, vals): @api.constrains("bom_id")
for bl in self.bom_id.bom_line_ids.filtered("component_template_id"): def _check_component_attributes(self):
bl._check_component_attributes() self.bom_id._check_component_attributes()
return super().write(vals)

View File

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

View File

@@ -1,3 +1,7 @@
* Ooops404 <https://ooops404.com> * Ooops404 <https://ooops404.com>
* Ilyas * 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> # @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # 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): class ReportBomStructure(models.AbstractModel):
@@ -30,7 +30,8 @@ class ReportBomStructure(models.AbstractModel):
else: else:
line.product_id = line_product line.product_id = line_product
if to_ignore_line_ids: 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( components, total = super()._get_bom_lines(
bom, bom_quantity, product, line_id, level bom, bom_quantity, product, line_id, level
) )

View File

@@ -1,168 +1,134 @@
from odoo import Command from odoo.tests import Form, common
from odoo.models import BaseModel
from odoo.tests import Form, TransactionCase
class TestMrpBomAttributeMatchBase(TransactionCase): class TestMrpAttachmentMgmtBase(common.SavepointCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls._create_products(cls)
cls.warehouse = cls.env.ref("stock.warehouse0") cls._create_boms(cls)
cls.route_manufacture = cls.warehouse.manufacture_pull_id.route_id
# Create products def _create_products(self):
cls.product_sword = cls.env["product.template"].create( 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", "name": "Plastic Sword",
"type": "product", "type": "product",
} }
) )
cls.product_surf = cls.env["product.template"].create( self.product_surf = self.env["product.template"].create(
{ {
"name": "Surf", "name": "Surf",
"type": "product", "type": "product",
} }
) )
cls.product_fin = cls.env["product.template"].create( self.product_fin = self.env["product.template"].create(
{ {
"name": "Surf Fin", "name": "Surf Fin",
"type": "product", "type": "product",
} }
) )
cls.product_plastic = cls.env["product.template"].create( self.product_plastic = self.env["product.template"].create(
{ {
"name": "Plastic Component", "name": "Plastic Component",
"type": "product", "type": "product",
} }
) )
cls.p1 = cls.env["product.template"].create( self.p1 = self.env["product.template"].create(
{ {
"name": "P1", "name": "P1",
"type": "product", "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", "name": "P2",
"type": "product", "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", "name": "P3",
"type": "product", "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", "name": "Paper",
} }
) )
cls.product_10 = cls.env["product.product"].create( self.product_10 = self.env["product.product"].create(
{ {
"name": "Stone", "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"} {"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": "Cyan", "attribute_id": self.product_attribute.id},
{"name": "Magenta", "attribute_id": cls.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, "attribute_id": self.product_attribute.id,
"product_tmpl_id": cls.product_plastic.id, "product_tmpl_id": self.product_plastic.id,
"value_ids": [Command.set(cls.product_attribute.value_ids.ids)], "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, "attribute_id": self.product_attribute.id,
"product_tmpl_id": cls.product_sword.id, "product_tmpl_id": self.product_sword.id,
"value_ids": [Command.set(cls.product_attribute.value_ids.ids)], "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_boms(self):
def _create_bom(cls, product, line_form_vals): mrp_bom_form = Form(self.env["mrp.bom"])
if product._name == "product.template": mrp_bom_form.product_tmpl_id = self.product_sword
template = product with mrp_bom_form.bom_line_ids.new() as line_form:
product = cls.env["product.product"] line_form.component_template_id = self.product_plastic
else: line_form.product_qty = 1
template = product.product_tmpl_id self.bom_id = mrp_bom_form.save()
with Form(cls.env["mrp.bom"]) as form:
form.product_tmpl_id = template mrp_bom_form = Form(self.env["mrp.bom"])
form.product_id = product mrp_bom_form.product_tmpl_id = self.product_fin
for vals in line_form_vals: with mrp_bom_form.bom_line_ids.new() as line_form:
with form.bom_line_ids.new() as line_form: line_form.product_id = self.product_plastic.product_variant_ids[0]
for key, value in vals.items(): line_form.product_qty = 1
field = line_form._model._fields.get(key) self.fin_bom_id = mrp_bom_form.save()
if field and field.relational: # pragma: no cover
if value and not isinstance(value, BaseModel): mrp_bom_form = Form(self.env["mrp.bom"])
value = cls.env[field.comodel_name].browse(value) mrp_bom_form.product_tmpl_id = self.product_surf
elif not value: with mrp_bom_form.bom_line_ids.new() as line_form:
value = cls.env[field.comodel_name] line_form.product_id = self.product_fin.product_variant_ids[0]
setattr(line_form, key, value) line_form.product_qty = 1
return form.save() 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.exceptions import UserError, ValidationError
from odoo.tests import Form 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): def test_bom_1(self):
mrp_bom_form = Form(self.env["mrp.bom"]) mrp_bom_form = Form(self.env["mrp.bom"])
mrp_bom_form.product_tmpl_id = self.product_sword 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.component_template_id = self.product_plastic
line_form.product_qty = 1 line_form.product_qty = 1
sword_cyan = self.sword_attrs.product_template_value_ids[0] sword_cyan = self.sword_attrs.product_template_value_ids[0]
with self.assertRaisesRegex( with self.assertRaises(ValidationError):
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 .*",
):
line_form.bom_product_template_attribute_value_ids.add(sword_cyan) line_form.bom_product_template_attribute_value_ids.add(sword_cyan)
def test_bom_2(self): def test_bom_2(self):
@@ -43,12 +42,7 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
"value_ids": [(4, orchid_attribute_value_id.id)], "value_ids": [(4, orchid_attribute_value_id.id)],
} }
) )
with self.assertRaisesRegex( with self.assertRaises(UserError):
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\.",
):
vals = { vals = {
"attribute_id": smell_attribute.id, "attribute_id": smell_attribute.id,
"product_tmpl_id": self.product_plastic.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 = Form(self.env["mrp.bom"])
mrp_bom_form.product_tmpl_id = self.product_sword mrp_bom_form.product_tmpl_id = self.product_sword
with mrp_bom_form.bom_line_ids.new() as line_form: with mrp_bom_form.bom_line_ids.new() as line_form:
with self.assertRaisesRegex( with self.assertRaises(ValidationError):
UserError,
r"Some attributes of the dynamic component are not included into "
r"production product attributes\.",
):
line_form.component_template_id = self.product_plastic line_form.component_template_id = self.product_plastic
plastic_smells_like_orchid.unlink() plastic_smells_like_orchid.unlink()
@@ -103,10 +93,8 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
# Component skipped # Component skipped
mo_form.bom_id = self.bom_id mo_form.bom_id = self.bom_id
mo_form.product_qty = 1 mo_form.product_qty = 1
with self.assertRaisesRegex( with self.assertRaises(ValidationError):
ValidationError, # Some attributes of the dynamic component are not included into ...
r"Some attributes of the dynamic component are not included into .+",
):
self.mo_sword = mo_form.save() self.mo_sword = mo_form.save()
def test_manufacturing_order_4(self): def test_manufacturing_order_4(self):
@@ -168,20 +156,5 @@ class TestMrpBomAttributeMatch(TestMrpBomAttributeMatchBase):
"product_qty": 1.0, "product_qty": 1.0,
} }
) )
with self.assertRaisesRegex(UserError, r"Recursion error! .+"): with self.assertRaises(UserError):
test_bom_3.explode(self.product_9, 1) 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" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="mrp_bom_view_form_inherit_bom_match" model="ir.ui.view"> <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="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view" /> <field name="inherit_id" ref="mrp.mrp_bom_form_view" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="bom_product_template_attribute_value_ids" position="before"> <field name="bom_product_template_attribute_value_ids" position="before">
<field <field name="match_on_attribute_ids" widget="many2many_tags" />
name="match_on_attribute_ids"
readonly='1'
force_save="1"
widget="many2many_tags"
/>
<field name="product_backup_id" invisible="1" /> <field name="product_backup_id" invisible="1" />
</field> </field>
<xpath <xpath