mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
When creating a new line, an onchange is played before the user selects the product. With the previous code, since no product was selected, this was emptying the default value. This was in turn breaking any other function that relied on this value being set.
385 lines
15 KiB
Python
385 lines
15 KiB
Python
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)",
|
|
domain="[('id', '!=', parent_product_tmpl_id)]",
|
|
)
|
|
match_on_attribute_ids = fields.Many2many(
|
|
"product.attribute",
|
|
string="Match on Attributes",
|
|
compute="_compute_match_on_attribute_ids",
|
|
store=True,
|
|
)
|
|
product_uom_category_id = fields.Many2one(
|
|
"uom.category",
|
|
related=None,
|
|
compute="_compute_product_uom_category_id",
|
|
)
|
|
|
|
@api.constrains("component_template_id")
|
|
def _check_component_template_recursion(self):
|
|
for line in self:
|
|
if line.component_template_id == line.parent_product_tmpl_id:
|
|
raise ValidationError(
|
|
_(
|
|
"Component template must be different from BOM "
|
|
"product template. Please check BOM: %s BOM Line: %s"
|
|
)
|
|
% (line, line.bom_id)
|
|
)
|
|
|
|
@api.depends("product_id", "component_template_id")
|
|
def _compute_product_uom_category_id(self):
|
|
"""Compute the product_uom_category_id field.
|
|
|
|
This is the product category that will be allowed to use on the product_uom_id
|
|
field, already covered by core module:
|
|
https://github.com/odoo/odoo/blob/331b9435c/addons/mrp/models/mrp_bom.py#L372
|
|
|
|
In core, though, this field is related to "product_id.uom_id.category_id".
|
|
Here we make it computed to choose between component_template_id and
|
|
product_id, depending on which one is set
|
|
"""
|
|
# pylint: disable=missing-return
|
|
# NOTE: To play nice with other modules trying to do the same:
|
|
# 1) Set the field value as if it were a related field (core behaviour)
|
|
# 2) Call super (if it's there)
|
|
# 3) Update only the records we want
|
|
for rec in self:
|
|
rec.product_uom_category_id = rec.product_id.uom_id.category_id
|
|
if hasattr(super(), "_compute_product_uom_category_id"):
|
|
super()._compute_product_uom_category_id()
|
|
for rec in self:
|
|
if rec.component_template_id:
|
|
rec.product_uom_category_id = (
|
|
rec.component_template_id.uom_id.category_id
|
|
)
|
|
|
|
@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_id.uom_id
|
|
and self.product_uom_id.category_id
|
|
!= self.product_id.uom_id.category_id
|
|
):
|
|
self.product_uom_id = self.product_id.uom_id
|
|
|
|
@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):
|
|
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
|
|
)
|
|
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_bom_product_template_attribute_value_ids_check_variants(self):
|
|
if self.bom_product_template_attribute_value_ids:
|
|
self._check_variants_validity()
|
|
|
|
def _skip_bom_line(self, product):
|
|
# Make this method compatible to work with NewIds
|
|
res = super()._skip_bom_line(product)
|
|
|
|
return res and (
|
|
len(
|
|
set(product.product_template_attribute_value_ids.ids)
|
|
& set(self.bom_product_template_attribute_value_ids.ids)
|
|
)
|
|
!= len(self.bom_product_template_attribute_value_ids.attribute_id)
|
|
)
|
|
|
|
|
|
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.sudo().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
|
|
|
|
@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()
|