mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[14.0][ADD] mrp_bom_attribute_match
This commit is contained in:
0
mrp_bom_attribute_match/README.rst
Normal file
0
mrp_bom_attribute_match/README.rst
Normal file
1
mrp_bom_attribute_match/__init__.py
Normal file
1
mrp_bom_attribute_match/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
16
mrp_bom_attribute_match/__manifest__.py
Normal file
16
mrp_bom_attribute_match/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
157
mrp_bom_attribute_match/i18n/it.po
Normal file
157
mrp_bom_attribute_match/i18n/it.po
Normal 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 it’s 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"
|
||||||
117
mrp_bom_attribute_match/i18n/mrp_bom_attribute_match.pot
Normal file
117
mrp_bom_attribute_match/i18n/mrp_bom_attribute_match.pot
Normal 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 it’s the same attribute used in field “Match on "
|
||||||
|
"Attribute”related to the component %s."
|
||||||
|
msgstr ""
|
||||||
3
mrp_bom_attribute_match/models/__init__.py
Normal file
3
mrp_bom_attribute_match/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import mrp_bom
|
||||||
|
from . import mrp_production
|
||||||
|
from . import product
|
||||||
297
mrp_bom_attribute_match/models/mrp_bom.py
Normal file
297
mrp_bom_attribute_match/models/mrp_bom.py
Normal 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 it’s 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
|
||||||
19
mrp_bom_attribute_match/models/mrp_production.py
Normal file
19
mrp_bom_attribute_match/models/mrp_production.py
Normal 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)
|
||||||
67
mrp_bom_attribute_match/models/product.py
Normal file
67
mrp_bom_attribute_match/models/product.py
Normal 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
|
||||||
3
mrp_bom_attribute_match/readme/CONTRIBUTORS.rst
Normal file
3
mrp_bom_attribute_match/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* Ooops404 <https://ooops404.com>
|
||||||
|
|
||||||
|
* Ilyas
|
||||||
38
mrp_bom_attribute_match/readme/DESCRIPTION.rst
Normal file
38
mrp_bom_attribute_match/readme/DESCRIPTION.rst
Normal 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.
|
||||||
20
mrp_bom_attribute_match/readme/USAGE.rst
Normal file
20
mrp_bom_attribute_match/readme/USAGE.rst
Normal 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.
|
||||||
1
mrp_bom_attribute_match/tests/__init__.py
Normal file
1
mrp_bom_attribute_match/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_mrp_bom_attribute_match
|
||||||
134
mrp_bom_attribute_match/tests/common.py
Normal file
134
mrp_bom_attribute_match/tests/common.py
Normal 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()
|
||||||
160
mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match.py
Normal file
160
mrp_bom_attribute_match/tests/test_mrp_bom_attribute_match.py
Normal 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)
|
||||||
34
mrp_bom_attribute_match/views/mrp_bom_views.xml
Normal file
34
mrp_bom_attribute_match/views/mrp_bom_views.xml
Normal 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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../mrp_bom_attribute_match
|
||||||
6
setup/mrp_bom_attribute_match/setup.py
Normal file
6
setup/mrp_bom_attribute_match/setup.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
setup_requires=['setuptools-odoo'],
|
||||||
|
odoo_addon=True,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user