[IMP] mrp_bom_attribute_match: use constraints and computed fields

Instead of updates triggered by onchanges or writes, use constraints
to execute all checks, and a computed field for `match_on_attribute_ids`
This commit is contained in:
Ivàn Todorovich
2022-10-27 17:19:24 -03:00
parent 30a41e1711
commit 5dde8e261e
4 changed files with 119 additions and 115 deletions

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,84 +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,
)
)
class MrpBom(models.Model):
_inherit = "mrp.bom"
@@ -333,8 +345,10 @@ class MrpBom(models.Model):
else:
return line_product_id
def write(self, vals):
res = super().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

@@ -5,12 +5,7 @@
<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