Files
manufacture/mrp_bom_attribute_match/models/mrp_bom.py

317 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_round
_log = logging.getLogger(__name__)
class MrpBomLine(models.Model):
_inherit = "mrp.bom.line"
product_id = fields.Many2one("product.product", "Component", required=False)
product_backup_id = fields.Many2one(
"product.product", help="Technical field to store previous value of product_id"
)
component_template_id = fields.Many2one(
"product.template", "Component (product template)"
)
match_on_attribute_ids = fields.Many2many(
"product.attribute", string="Match on Attributes", readonly=True
)
product_uom_id_domain = fields.Char(compute="_compute_product_uom_id_domain")
@api.depends("component_template_id", "product_id")
def _compute_product_uom_id_domain(self):
for r in self:
if r.component_template_id:
category_id = r.component_template_id.uom_id.category_id.id
if (
r.product_uom_id.category_id.id
!= r.component_template_id.uom_id.category_id.id
):
r.product_uom_id = r.component_template_id.uom_id
else:
category_id = r.product_uom_category_id.id
r.product_uom_id_domain = json.dumps([("category_id", "=", category_id)])
@api.onchange("component_template_id")
def _onchange_component_template_id(self):
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
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
)
)
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."
)
)
@api.onchange("bom_product_template_attribute_value_ids")
def _onchange_attribute_value_ids(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:
raise ValidationError(
_(
"You cannot use an attribute value for attribute"
" %s in the field “Apply on Variants” as its the"
" same attribute used in field “Match on Attribute”"
"related to the component %s."
% (
self.env["product.attribute"].browse(same_attrs),
self.component_template_id.name,
)
)
)
def write(self, vals):
super(MrpBomLine, self).write(vals)
class MrpBom(models.Model):
_inherit = "mrp.bom"
# flake8: noqa: C901
def explode(self, product, quantity, picking_type=False):
# Had to replace this method
"""
Explodes the BoM and creates two lists with all the information you need:
bom_done and line_done
Quantity describes the number of times you need the BoM: so the quantity
divided by the number created by the BoM
and converted into its UoM
"""
from collections import defaultdict
graph = defaultdict(list)
V = set()
def check_cycle(v, visited, recStack, graph):
visited[v] = True
recStack[v] = True
for neighbour in graph[v]:
if visited[neighbour] is False:
if check_cycle(neighbour, visited, recStack, graph) is True:
return True
elif recStack[neighbour] is True:
return True
recStack[v] = False
return False
product_ids = set()
product_boms = {}
def update_product_boms():
products = self.env["product.product"].browse(product_ids)
product_boms.update(
self._get_product2bom(
products,
bom_type="phantom",
picking_type=picking_type or self.picking_type_id,
company_id=self.company_id.id,
)
)
# Set missing keys to default value
for product in products:
product_boms.setdefault(product, self.env["mrp.bom"])
boms_done = [
(
self,
{
"qty": quantity,
"product": product,
"original_qty": quantity,
"parent_line": False,
},
)
]
lines_done = []
V |= {product.product_tmpl_id.id}
bom_lines = []
for bom_line in self.bom_line_ids:
product_id = bom_line.product_id
V |= {product_id.product_tmpl_id.id}
graph[product.product_tmpl_id.id].append(product_id.product_tmpl_id.id)
bom_lines.append((bom_line, product, quantity, False))
product_ids.add(product_id.id)
update_product_boms()
product_ids.clear()
while bom_lines:
current_line, current_product, current_qty, parent_line = bom_lines[0]
bom_lines = bom_lines[1:]
if current_line._skip_bom_line(current_product):
continue
line_quantity = current_qty * current_line.product_qty
if current_line.product_id not in product_boms:
update_product_boms()
product_ids.clear()
# upd start
component_template_product = self.get_component_template_product(
current_line, product, current_line.product_id
)
if component_template_product:
# need to set product_id temporary
current_line.product_id = component_template_product
else:
# component_template_id is set, but no attribute value match.
continue
# upd end
bom = product_boms.get(current_line.product_id)
if bom:
converted_line_quantity = current_line.product_uom_id._compute_quantity(
line_quantity / bom.product_qty, bom.product_uom_id
)
bom_lines += [
(
line,
current_line.product_id,
converted_line_quantity,
current_line,
)
for line in bom.bom_line_ids
]
for bom_line in bom.bom_line_ids:
graph[current_line.product_id.product_tmpl_id.id].append(
bom_line.product_id.product_tmpl_id.id
)
if bom_line.product_id.product_tmpl_id.id in V and check_cycle(
bom_line.product_id.product_tmpl_id.id,
{key: False for key in V},
{key: False for key in V},
graph,
):
raise UserError(
_(
"Recursion error! A product with a Bill of Material "
"should not have itself in its BoM or child BoMs!"
)
)
V |= {bom_line.product_id.product_tmpl_id.id}
if bom_line.product_id not in product_boms:
product_ids.add(bom_line.product_id.id)
boms_done.append(
(
bom,
{
"qty": converted_line_quantity,
"product": current_product,
"original_qty": quantity,
"parent_line": current_line,
},
)
)
else:
# We round up here because the user expects
# that if he has to consume a little more, the whole UOM unit
# should be consumed.
rounding = current_line.product_uom_id.rounding
line_quantity = float_round(
line_quantity, precision_rounding=rounding, rounding_method="UP"
)
lines_done.append(
(
current_line,
{
"qty": line_quantity,
"product": current_product,
"original_qty": quantity,
"parent_line": parent_line,
},
)
)
return boms_done, lines_done
def get_component_template_product(self, bom_line, bom_product_id, line_product_id):
if bom_line.component_template_id:
comp = bom_line.component_template_id
comp_attr_ids = (
comp.valid_product_template_attribute_line_ids.attribute_id.ids
)
prod_attr_ids = (
bom_product_id.valid_product_template_attribute_line_ids.attribute_id.ids
)
# check attributes
if not all(item in prod_attr_ids for item in comp_attr_ids):
_log.info(
"Component skipped. Component attributes must be included into "
"product attributes to use component_template_id."
)
return False
# find matching combination
combination = self.env["product.template.attribute.value"]
for ptav in bom_product_id.product_template_attribute_value_ids:
combination |= self.env["product.template.attribute.value"].search(
[
("product_tmpl_id", "=", comp.id),
("attribute_id", "=", ptav.attribute_id.id),
(
"product_attribute_value_id",
"=",
ptav.product_attribute_value_id.id,
),
]
)
if len(combination) == 0:
return False
product_id = comp._get_variant_for_combination(combination)
if product_id and product_id.active:
return product_id
return False
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