[14.0][ADD] mrp_bom_attribute_match

This commit is contained in:
Ilyas
2022-06-17 19:31:21 +07:00
parent 32fc950de2
commit 215cfe1519
18 changed files with 1074 additions and 0 deletions

View File

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,16 @@
{
"name": "BOM Attribute Match",
"version": "14.0.1.0.1",
"category": "Manufacturing",
"author": "Ilyas, Ooops, Odoo Community Association (OCA)",
"summary": "Dynamic BOM component based on product attribute",
"depends": ["mrp_account"],
"license": "LGPL-3",
"website": "https://github.com/OCA/manufacture",
"data": [
"views/mrp_bom_views.xml",
],
"qweb": [],
"application": False,
"installable": True,
}

View File

@@ -0,0 +1,157 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mrp_bom_attribute_match
#
# Francesco @ Ooops <francesco.foresti@ooops404.com>, 2022.
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-14 17:50+0000\n"
"PO-Revision-Date: 2022-03-14 13:36+0000\n"
"Last-Translator: Francesco @ Ooops <francesco.foresti@ooops404.com>\n"
"Language-Team: Italian <http://weblate.ops404.it/projects/ooops-mrp-14-0/"
"mrp_bom_attribute_match/it/>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.4-dev\n"
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_mrp_bom
msgid "Bill of Material"
msgstr "Distinta base"
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_mrp_bom_line
msgid "Bill of Material Line"
msgstr "Riga Distinta Base"
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__product_id
msgid "Component"
msgstr "Componente"
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__component_template_id
msgid "Component (product template)"
msgstr "Componente (modello prodotto)"
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom__display_name
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__display_name
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_production__display_name
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_product_template__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom__id
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__id
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_production__id
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_product_template__id
msgid "ID"
msgstr "ID"
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom____last_update
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line____last_update
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_production____last_update
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_product_template____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__match_on_attribute_ids
msgid "Match on Attributes"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__product_backup_id
msgid "Product Backup"
msgstr "Backup componente"
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_product_template
msgid "Product Template"
msgstr "Modello Prodotto"
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_mrp_production
msgid "Production Order"
msgstr "Ordine di Produzione"
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/mrp_bom.py:0
#, python-format
msgid ""
"Recursion error! A product with a Bill of Material should not have itself "
"in its BoM or child BoMs!"
msgstr ""
"Errore ricorsivo! Un prodotto con una distinta base non può essere presente "
"nella sua DiBa o nelle DiBa figlie!"
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/mrp_bom.py:0
#, python-format
msgid ""
"Some attributes of the dynamic component are not included into production "
"product attributes."
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,help:mrp_bom_attribute_match.field_mrp_bom_line__product_backup_id
msgid "Technical field to store previous value of product_id"
msgstr "Campo tecnico per preservare i valori di product_id"
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/product.py:0
#, python-format
msgid ""
"This product template is used as a component in the BOMs for %s and "
"attribute(s) %s is not present in all such product(s), and this would break "
"the BOM behavior."
msgstr ""
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/mrp_bom.py:0
#, python-format
msgid ""
"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."
msgstr ""
"Non puoi utilizzare un valore attributo dell'attributo %s nel campo “Applica "
"alle varianti” dato che è lo stesso attributo utilizzato nel campo "
"“Corrispondenza su attributo” relativo al componente %s."
#~ msgid "Dynamic component must have only 1 attribute"
#~ msgstr "Il componente dinamico deve avere un solo attributo"
#~ msgid "Match on Attribute"
#~ msgstr "Corrispondenza su attributo"
#~ msgid "Only product template with one attribute can be added to this field."
#~ msgstr ""
#~ "Solo i modelli prodotto con un solo attributo possono essere aggiunti a "
#~ "questo campo."
#~ msgid ""
#~ "This product included into BOMs as dynamic component. Please remove it "
#~ "from related BOMs to be able to have multiple attributes for it. BOM ids: "
#~ "%s"
#~ msgstr ""
#~ "Questo prodotto è incluso in una distinta base come componente dinamico. "
#~ "Per aggiungere altri attributi, devi rimuoverlo dalle seguenti DiBa: %s"
#~ msgid "Attribute Value Backup"
#~ msgstr "Backup Valori Attributo"
#~ msgid "Match On Attribute"
#~ msgstr "Corrispondenza su attributo"
#~ msgid "Technical field to store previous value of attribute_value_ids"
#~ msgstr ""
#~ "Campo tecnico per preservare i valori precedenti di attribute_value_ids"

View File

@@ -0,0 +1,117 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mrp_bom_attribute_match
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_mrp_bom
msgid "Bill of Material"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_mrp_bom_line
msgid "Bill of Material Line"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__product_id
msgid "Component"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__component_template_id
msgid "Component (product template)"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom__display_name
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__display_name
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_production__display_name
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_product_template__display_name
msgid "Display Name"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom__id
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__id
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_production__id
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_product_template__id
msgid "ID"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom____last_update
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line____last_update
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_production____last_update
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_product_template____last_update
msgid "Last Modified on"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__match_on_attribute_ids
msgid "Match on Attributes"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,field_description:mrp_bom_attribute_match.field_mrp_bom_line__product_backup_id
msgid "Product Backup"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_product_template
msgid "Product Template"
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model,name:mrp_bom_attribute_match.model_mrp_production
msgid "Production Order"
msgstr ""
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/mrp_bom.py:0
#, python-format
msgid ""
"Recursion error! A product with a Bill of Material should not have itself "
"in its BoM or child BoMs!"
msgstr ""
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/mrp_bom.py:0
#, python-format
msgid ""
"Some attributes of the dynamic component are not included into production "
"product attributes."
msgstr ""
#. module: mrp_bom_attribute_match
#: model:ir.model.fields,help:mrp_bom_attribute_match.field_mrp_bom_line__product_backup_id
msgid "Technical field to store previous value of product_id"
msgstr ""
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/product.py:0
#, python-format
msgid ""
"This product template is used as a component in the BOMs for %s and "
"attribute(s) %s is not present in all such product(s), and this would break "
"the BOM behavior."
msgstr ""
#. module: mrp_bom_attribute_match
#: code:addons/mrp_bom_attribute_match/models/mrp_bom.py:0
#, python-format
msgid ""
"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."
msgstr ""

View File

@@ -0,0 +1,3 @@
from . import mrp_bom
from . import mrp_production
from . import product

View File

@@ -0,0 +1,297 @@
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
)
@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,
)
)
)
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

View File

@@ -0,0 +1,19 @@
from odoo import models
class MrpProduction(models.Model):
_inherit = "mrp.production"
def action_confirm(self):
res = super(MrpProduction, self).action_confirm()
if self and self.bom_id:
for bom_line in self.bom_id.bom_line_ids:
if bom_line.component_template_id:
# product_id was set in mrp.bom.explode for correct flow. Need to remove it.
bom_line.product_id = False
return res
def write(self, vals):
for bl in self.bom_id.bom_line_ids.filtered(lambda x: x.component_template_id):
bl.check_component_attributes()
return super(MrpProduction, self).write(vals)

View File

@@ -0,0 +1,67 @@
from odoo import _, models
from odoo.exceptions import UserError
class ProductTemplate(models.Model):
_inherit = "product.template"
def write(self, vals):
res = super(ProductTemplate, self).write(vals)
self.check_product_with_component_change_allowed()
self.check_component_change_allowed()
return res
def check_product_with_component_change_allowed(self):
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(
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)
raise UserError(
_(
"The attributes you're trying to remove is used in BoM "
"as a match with Component (Product Template). To "
"remove these attributes, first remove the BOM line "
"with the matching component.\nAttributes: %s\nBoM: %s"
% (attr_recs.mapped("name"), bom.display_name)
)
)
def check_component_change_allowed(self):
if len(self.attribute_line_ids) > 0:
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 %s and attribute(s) %s is not present in all "
"such product(s), and this would break the BOM "
"behavior."
% (
bom.display_name,
attr_recs.mapped("name"),
)
)
)
def get_component_boms(self):
bom_lines = self.env["mrp.bom.line"].search(
[("component_template_id", "=", self._origin.id)]
)
if bom_lines:
return bom_lines.mapped("bom_id")
return False

View File

@@ -0,0 +1,3 @@
* Ooops404 <https://ooops404.com>
* Ilyas

View File

@@ -0,0 +1,38 @@
This module addresses the BoM case where the product to manufacture has one attribute with tens or hundreds of values (usually attribute "color", eg: "Configurable Desk" can be produced in 900 different colors).
Creating a dynamic BoM currently requires adding one BoM line for each attribute value to match component variant with attribute value (eg: component "Desk board (Green)" to be applied to variant "Green").
This has 3 downsides:
- BoM lines proliferation (more error prone)
- Difficult to update in case a new attribute value (new color paint) is added
- Difficult to update in case base component changes.
This module allows to use a product template as component in BoM lines, automatically matching component variant to use in MO line with the attribute value selected for manufacture.
Eg: Product template "Desk Board" is added to BoM line for product "Configurable Desk"; match is made on attribute "Color". In MO, if product to manufacture is "Configurable Desk (Steel, Pink)", MO line will have component "Desk Board (Pink)".
Using the same BoM, if product to manufacture is "Configurable Desk (Steel, Yellow)", MO line will have component "Desk Board (Yellow)".
The flow is valid also if the Component (Product Template) has more than one attribute matching the product to manufacture; in this case, on MO line the component variant will be the one matching multiple attribute values for the product to manufacture.
Various checks are in place to make sure this flow is not disrupted:
- user cannot add a product in field "Component (Product Template)" which:
does not have matching attributes with product to manufacture
has a different variant-generating attribute than the product to manufacture
- Adding a new variant-generating attribute to a product used as "Component (Product Template)" raises an error if the attribute is not included in all the products to manufacture where component is referenced.
- Removing an attribute used for BoM attribute matching from product to manufacture raises an error.
- On a BoM line with Component (Product Template) set, an attribute value of attributes referenced in "Match on attribute" field cannot be used in field "Apply to variant".
- If attribute value for matching attribute in manufactured product is not present in component (product template), the BoM line is skipped in MO.

View File

@@ -0,0 +1,20 @@
Using this module you can have dynamic components of a BOM.
It will allow you to have only 1 line in the BOM if you have hundreds of attribute
values for manufacturing product and hundreds of attributes values of component (material).
How to use
#. Create a product to produce e.g. Desk.
#. Set 1 attribute (e.g. Color). And select possible values for it.
#. Create a component product (material) e.g. Plastic.
#. Set 1 attribute (Color). And select possible values for it.
#. Create a BOM.
#. Select a manufacturing product Desk.
#. Add a BOM line. Select Component (product template) Plastic.
#. You will see Color attribute appeared in the Apply On Attribute field.
#. Save the BOM.
#. Create Manufacturing Order. Select Desk with e.g. Red color to produce and BOM you created.
#. You will see in the component list Plastic added with corresponding (red) color.
Consider, that to use this feature component must have only 1 attribute.
And a values of this attribute of a manufacturing product should be available for a component.

View File

@@ -0,0 +1 @@
from . import test_mrp_bom_attribute_match

View File

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

@@ -0,0 +1,160 @@
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form
from .common import TestMrpAttachmentMgmtBase
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
with mrp_bom_form.bom_line_ids.new() as line_form:
line_form.product_id = self.product_plastic.product_variant_ids[0]
line_form.component_template_id = self.product_plastic
self.assertEqual(line_form.product_id.id, False)
line_form.component_template_id = self.env["product.template"]
self.assertEqual(
line_form.product_id, self.product_plastic.product_variant_ids[0]
)
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.assertRaises(ValidationError):
line_form.bom_product_template_attribute_value_ids.add(sword_cyan)
def test_bom_2(self):
smell_attribute = self.env["product.attribute"].create(
{"name": "Smell", "display_type": "radio", "create_variant": "always"}
)
orchid_attribute_value_id = self.env["product.attribute.value"].create(
[
{"name": "Orchid", "attribute_id": smell_attribute.id},
]
)
plastic_smells_like_orchid = self.env["product.template.attribute.line"].create(
{
"attribute_id": smell_attribute.id,
"product_tmpl_id": self.product_plastic.id,
"value_ids": [(4, orchid_attribute_value_id.id)],
}
)
with self.assertRaises(UserError):
vals = {
"attribute_id": smell_attribute.id,
"product_tmpl_id": self.product_plastic.id,
"value_ids": [(4, orchid_attribute_value_id.id)],
}
self.product_plastic.write({"attribute_line_ids": [(0, 0, vals)]})
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.assertRaises(ValidationError):
line_form.component_template_id = self.product_plastic
plastic_smells_like_orchid.unlink()
def test_manufacturing_order_1(self):
mo_form = Form(self.env["mrp.production"])
mo_form.product_id = self.product_sword.product_variant_ids.filtered(
lambda x: x.display_name == "Plastic Sword (Cyan)"
)
mo_form.bom_id = self.bom_id
mo_form.product_qty = 1
self.mo_sword = mo_form.save()
self.mo_sword.action_confirm()
# Assert correct component variant was selected automatically
self.assertEqual(
self.mo_sword.move_raw_ids.product_id.display_name,
"Plastic Component (Cyan)",
)
def test_manufacturing_order_2(self):
# Delete Cyan value from plastic
self.plastic_attrs.value_ids = [(3, self.plastic_attrs.value_ids[0].id, 0)]
mo_form = Form(self.env["mrp.production"])
mo_form.product_id = self.product_sword.product_variant_ids.filtered(
lambda x: x.display_name == "Plastic Sword (Cyan)"
)
mo_form.bom_id = self.bom_id
mo_form.product_qty = 1
self.mo_sword = mo_form.save()
with self.assertRaises(UserError):
# Add some materials to consume before marking this MO as to do.
self.mo_sword.action_confirm()
def test_manufacturing_order_3(self):
# Delete attribute from sword
self.product_sword.attribute_line_ids = [(5, 0, 0)]
mo_form = Form(self.env["mrp.production"])
mo_form.product_id = self.product_sword.product_variant_ids[0]
# Component skipped
mo_form.bom_id = self.bom_id
mo_form.product_qty = 1
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):
mo_form = Form(self.env["mrp.production"])
mo_form.product_id = self.product_surf.product_variant_ids[0]
mo_form.bom_id = self.surf_bom_id
mo_form.product_qty = 1
self.mo_sword = mo_form.save()
self.mo_sword.action_confirm()
# def test_manufacturing_order_5(self):
# mo_form = Form(self.env["mrp.production"])
# mo_form.product_id = self.product_surf.product_variant_ids[0]
# mo_form.bom_id = self.surf_wrong_bom_id
# mo_form.product_qty = 1
# self.mo_sword = mo_form.save()
# self.mo_sword.action_confirm()
# def test_manufacturing_order_6(self):
# mo_form = Form(self.env["mrp.production"])
# mo_form.product_id = self.p1.product_variant_ids[0]
# mo_form.bom_id = self.p1_bom_id
# mo_form.product_qty = 1
# self.mo_sword = mo_form.save()
# self.mo_sword.action_confirm()
def test_bom_recursion(self):
test_bom_3 = self.env["mrp.bom"].create(
{
"product_id": self.product_9.id,
"product_tmpl_id": self.product_9.product_tmpl_id.id,
"product_uom_id": self.product_9.uom_id.id,
"product_qty": 1.0,
"consumption": "flexible",
"type": "normal",
}
)
test_bom_4 = self.env["mrp.bom"].create(
{
"product_id": self.product_10.id,
"product_tmpl_id": self.product_10.product_tmpl_id.id,
"product_uom_id": self.product_10.uom_id.id,
"product_qty": 1.0,
"consumption": "flexible",
"type": "phantom",
}
)
self.env["mrp.bom.line"].create(
{
"bom_id": test_bom_3.id,
"product_id": self.product_10.id,
"product_qty": 1.0,
}
)
self.env["mrp.bom.line"].create(
{
"bom_id": test_bom_4.id,
"product_id": self.product_9.id,
"product_qty": 1.0,
}
)
with self.assertRaises(UserError):
test_bom_3.explode(self.product_9, 1)

View File

@@ -0,0 +1,34 @@
<?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="product_backup_id" invisible="1" />
</field>
<xpath
expr="//field[@name='bom_line_ids']//field[@name='product_id']"
position="after"
>
<field name="component_template_id" />
</xpath>
<xpath
expr="//field[@name='bom_line_ids']//field[@name='product_id']"
position="attributes"
>
<attribute name="attrs">
{'readonly': [('component_template_id', '!=', False)]}
</attribute>
<attribute name="force_save">"1"</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../mrp_bom_attribute_match

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)