diff --git a/mrp_bom_attribute_match_semifinished_product/README.rst b/mrp_bom_attribute_match_semifinished_product/README.rst new file mode 100644 index 000000000..0b19e2d07 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/README.rst @@ -0,0 +1,137 @@ +========================================= +BOM Attribute Match Semifinished Products +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d38945eaefcb9943ca73a4adc0b52a30e9181cc9fbc36e38f9eee796f68d3bde + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/14.0/mrp_bom_attribute_match_semifinished_product + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-mrp_bom_attribute_match_semifinished_product + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/manufacture&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module makes use of the features provided by module mrp_bom_attribute_match to create, for a finished product: + +* Semi-finished products with relevant attributes and settings +* Structure of BoMs to manufacture them. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +In industries like textile and apparel it’s common to have finished products with many variants for color and size, as well as several semi-finished products for various stages of manufacturing - often subcontracted. + +This module leaves to the user only the following manual steps: + +* creation of finished product +* creation of BoM for first semi-finished product + +automating the steps in the middle. + + + +Configuration +============= + +To allow a user to use features of this module, enable technical access right "Create finished product structure". + +Usage +===== + +In a product template > inventory tab, enable field “Finished product” (note: it can only be enabled for products with variants). + +From product template form, use server action “Create finished product structure”: a wizard is displayed with the following fields: + +* **Stage name**: this field is used to provide a name to the semi-finished product; the name will be set as *product template name* - *stage name* eg: “Customizable desk (CONFIG) - Wood polishing” + +* **Template product**: he semi-finished product will be created as a copy of the product template set in this field, with all the related settings (eg: routes, supplierinfo, product type…). + + Note: only product templates without attributes and values can be set in this field. + +* **Attribute(s)**: this field is used to set which attributes and values to copy from finished product to semi-finished product of this stage. + + Note: each semi-finished product cannot have more attributes than the semi-finished product in previous stage. + +* **BoM type**: the type of Bom to manufacture the previous product in the semi-finished product chain. If Bom Type = Subcontracting, it’s possible to set subcontractors. + + Note: in first stage, set type of BoM to manufacture finished product; in second stage, set type of BoM to manufacture first stage semi-finished product, and so on. + + +On creation, a BOM for finished product will be created, with: + +* BoM type (and subcontractors) from first line of wizard + +* semi-finished product from first line of wizard as “Component (product template)” in BoM lines + +* attribute match on semi-finished product attributes. + +If a second line was present in wizard, the same will happen for semi-finished product from first line, and so on. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Cetmix +* Ooops + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-geomer198| image:: https://github.com/geomer198.png?size=40px + :target: https://github.com/geomer198 + :alt: geomer198 +.. |maintainer-CetmixGitDrone| image:: https://github.com/CetmixGitDrone.png?size=40px + :target: https://github.com/CetmixGitDrone + :alt: CetmixGitDrone + +Current `maintainers `__: + +|maintainer-geomer198| |maintainer-CetmixGitDrone| + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_bom_attribute_match_semifinished_product/__init__.py b/mrp_bom_attribute_match_semifinished_product/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/mrp_bom_attribute_match_semifinished_product/__manifest__.py b/mrp_bom_attribute_match_semifinished_product/__manifest__.py new file mode 100644 index 000000000..6d6dca8dd --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "BOM Attribute Match Semifinished Products", + "version": "14.0.1.0.0", + "category": "Manufacturing", + "author": "Cetmix, Ooops, Odoo Community Association (OCA)", + "summary": "BOM Attribute Match Semifinished Products", + "depends": ["mrp_bom_attribute_match"], + "maintainers": ["geomer198", "CetmixGitDrone"], + "license": "AGPL-3", + "website": "https://github.com/OCA/manufacture", + "data": [ + "security/semifinished_product_security.xml", + "security/ir.model.access.csv", + "views/product_template_views.xml", + "views/semi_finished_product_template_line_views.xml", + "wizard/finished_product_structure_wizard.xml", + ], +} diff --git a/mrp_bom_attribute_match_semifinished_product/models/__init__.py b/mrp_bom_attribute_match_semifinished_product/models/__init__.py new file mode 100644 index 000000000..ae3f5c29d --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_template +from . import semi_finished_product_template_line diff --git a/mrp_bom_attribute_match_semifinished_product/models/product_template.py b/mrp_bom_attribute_match_semifinished_product/models/product_template.py new file mode 100644 index 000000000..bd7f77c39 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/models/product_template.py @@ -0,0 +1,47 @@ +from odoo import _, api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + finished_product = fields.Boolean() + semi_finished_product_tmpl_ids = fields.One2many( + comodel_name="semi.finished.product.template.line", + inverse_name="product_tmpl_id", + ) + semi_finished_mrp_bom_ids = fields.Many2many( + comodel_name="mrp.bom", string="MRP BoM" + ) + + @api.constrains("finished_product", "attribute_line_ids") + def _check_finished_product(self): + for rec in self.filtered("finished_product"): + if not rec.attribute_line_ids: + raise models.UserError( + _( + "Finished product is meant to be used only " + "on products with attributes in order " + "to create a BOM structure for its semi-finished " + "products based on attribute value match" + ) + ) + + def action_finished_product_structure(self): + self.ensure_one() + if not self.finished_product: + raise models.UserError( + _( + "You can only create finished product structure " + 'for products marked as "Finished product".' + ) + ) + return { + "name": _("Create finished product structure"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "finished.product.structure.wizard", + "context": { + "default_finished_product_id": self.id, + }, + "target": "new", + } diff --git a/mrp_bom_attribute_match_semifinished_product/models/semi_finished_product_template_line.py b/mrp_bom_attribute_match_semifinished_product/models/semi_finished_product_template_line.py new file mode 100644 index 000000000..696ed462f --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/models/semi_finished_product_template_line.py @@ -0,0 +1,24 @@ +from odoo import fields, models + + +class SemiFinishedProductTemplateLine(models.Model): + _name = "semi.finished.product.template.line" + + product_tmpl_id = fields.Many2one(comodel_name="product.template") + semi_finished_product_tmpl_id = fields.Many2one( + comodel_name="product.template", string="Semi-finished Product" + ) + attribute_ids = fields.Many2many( + comodel_name="product.attribute", + relation="semi_finished_product_template_line_rel", + ) + bom_type = fields.Selection( + selection=[ + ("normal", "Manufacture this product"), + ("phantom", "Kit"), + ("subcontract", "Subcontracting"), + ], + default="normal", + required=True, + ) + partner_ids = fields.Many2many(comodel_name="res.partner", string="Subcontractors") diff --git a/mrp_bom_attribute_match_semifinished_product/readme/CONFIGURE.rst b/mrp_bom_attribute_match_semifinished_product/readme/CONFIGURE.rst new file mode 100644 index 000000000..62cba30af --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/readme/CONFIGURE.rst @@ -0,0 +1 @@ +To allow a user to use features of this module, enable technical access right "Create finished product structure". diff --git a/mrp_bom_attribute_match_semifinished_product/readme/CONTEXT.rst b/mrp_bom_attribute_match_semifinished_product/readme/CONTEXT.rst new file mode 100644 index 000000000..8a7ab8d3f --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/readme/CONTEXT.rst @@ -0,0 +1,10 @@ +In industries like textile and apparel it’s common to have finished products with many variants for color and size, as well as several semi-finished products for various stages of manufacturing - often subcontracted. + +This module leaves to the user only the following manual steps: + +* creation of finished product +* creation of BoM for first semi-finished product + +automating the steps in the middle. + + diff --git a/mrp_bom_attribute_match_semifinished_product/readme/DESCRIPTION.rst b/mrp_bom_attribute_match_semifinished_product/readme/DESCRIPTION.rst new file mode 100644 index 000000000..db30a731d --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module makes use of the features provided by module mrp_bom_attribute_match to create, for a finished product: + +* Semi-finished products with relevant attributes and settings +* Structure of BoMs to manufacture them. diff --git a/mrp_bom_attribute_match_semifinished_product/readme/USAGE.rst b/mrp_bom_attribute_match_semifinished_product/readme/USAGE.rst new file mode 100644 index 000000000..de28ca6b0 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/readme/USAGE.rst @@ -0,0 +1,28 @@ +In a product template > inventory tab, enable field “Finished product” (note: it can only be enabled for products with variants). + +From product template form, use server action “Create finished product structure”: a wizard is displayed with the following fields: + +* **Stage name**: this field is used to provide a name to the semi-finished product; the name will be set as *product template name* - *stage name* eg: “Customizable desk (CONFIG) - Wood polishing” + +* **Template product**: he semi-finished product will be created as a copy of the product template set in this field, with all the related settings (eg: routes, supplierinfo, product type…). + + Note: only product templates without attributes and values can be set in this field. + +* **Attribute(s)**: this field is used to set which attributes and values to copy from finished product to semi-finished product of this stage. + + Note: each semi-finished product cannot have more attributes than the semi-finished product in previous stage. + +* **BoM type**: the type of Bom to manufacture the previous product in the semi-finished product chain. If Bom Type = Subcontracting, it’s possible to set subcontractors. + + Note: in first stage, set type of BoM to manufacture finished product; in second stage, set type of BoM to manufacture first stage semi-finished product, and so on. + + +On creation, a BOM for finished product will be created, with: + +* BoM type (and subcontractors) from first line of wizard + +* semi-finished product from first line of wizard as “Component (product template)” in BoM lines + +* attribute match on semi-finished product attributes. + +If a second line was present in wizard, the same will happen for semi-finished product from first line, and so on. diff --git a/mrp_bom_attribute_match_semifinished_product/readme/newsfragments/.gitkeep b/mrp_bom_attribute_match_semifinished_product/readme/newsfragments/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/mrp_bom_attribute_match_semifinished_product/security/ir.model.access.csv b/mrp_bom_attribute_match_semifinished_product/security/ir.model.access.csv new file mode 100644 index 000000000..ea5c8790d --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_finished_product_structure_wizard,mrp.finished.product.structure.wizard,model_finished_product_structure_wizard,mrp.group_mrp_user,1,1,1,1 +access_finished_product_structure_line,mrp.finished.product.structure.line,model_finished_product_structure_line,mrp.group_mrp_user,1,1,1,1 +access_semi_finished_product_template_line,mrp.semi.finished.product.template.line,model_semi_finished_product_template_line,mrp.group_mrp_user,1,1,1,1 diff --git a/mrp_bom_attribute_match_semifinished_product/security/semifinished_product_security.xml b/mrp_bom_attribute_match_semifinished_product/security/semifinished_product_security.xml new file mode 100644 index 000000000..fc977b900 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/security/semifinished_product_security.xml @@ -0,0 +1,10 @@ + + + + + Create finished product structure + + + + + diff --git a/mrp_bom_attribute_match_semifinished_product/static/description/index.html b/mrp_bom_attribute_match_semifinished_product/static/description/index.html new file mode 100644 index 000000000..099325709 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/static/description/index.html @@ -0,0 +1,463 @@ + + + + + + +BOM Attribute Match Semifinished Products + + + +
+

BOM Attribute Match Semifinished Products

+ + +

Beta License: AGPL-3 OCA/manufacture Translate me on Weblate Try me on Runboat

+

This module makes use of the features provided by module mrp_bom_attribute_match to create, for a finished product:

+
    +
  • Semi-finished products with relevant attributes and settings
  • +
  • Structure of BoMs to manufacture them.
  • +
+

Table of contents

+ +
+

Use Cases / Context

+

In industries like textile and apparel it’s common to have finished products with many variants for color and size, as well as several semi-finished products for various stages of manufacturing - often subcontracted.

+

This module leaves to the user only the following manual steps:

+
    +
  • creation of finished product
  • +
  • creation of BoM for first semi-finished product
  • +
+

automating the steps in the middle.

+
+
+

Configuration

+

To allow a user to use features of this module, enable technical access right “Create finished product structure”.

+
+
+

Usage

+

In a product template > inventory tab, enable field “Finished product” (note: it can only be enabled for products with variants).

+

From product template form, use server action “Create finished product structure”: a wizard is displayed with the following fields:

+
    +
  • Stage name: this field is used to provide a name to the semi-finished product; the name will be set as product template name - stage name eg: “Customizable desk (CONFIG) - Wood polishing”

    +
  • +
  • Template product: he semi-finished product will be created as a copy of the product template set in this field, with all the related settings (eg: routes, supplierinfo, product type…).

    +

    Note: only product templates without attributes and values can be set in this field.

    +
  • +
  • Attribute(s): this field is used to set which attributes and values to copy from finished product to semi-finished product of this stage.

    +

    Note: each semi-finished product cannot have more attributes than the semi-finished product in previous stage.

    +
  • +
  • BoM type: the type of Bom to manufacture the previous product in the semi-finished product chain. If Bom Type = Subcontracting, it’s possible to set subcontractors.

    +

    Note: in first stage, set type of BoM to manufacture finished product; in second stage, set type of BoM to manufacture first stage semi-finished product, and so on.

    +
  • +
+

On creation, a BOM for finished product will be created, with:

+
    +
  • BoM type (and subcontractors) from first line of wizard
  • +
  • semi-finished product from first line of wizard as “Component (product template)” in BoM lines
  • +
  • attribute match on semi-finished product attributes.
  • +
+

If a second line was present in wizard, the same will happen for semi-finished product from first line, and so on.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Cetmix
  • +
  • Ooops
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

geomer198 CetmixGitDrone

+

This module is part of the OCA/manufacture project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/mrp_bom_attribute_match_semifinished_product/tests/__init__.py b/mrp_bom_attribute_match_semifinished_product/tests/__init__.py new file mode 100644 index 000000000..285f6cc0a --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_finished_product_common +from . import test_finished_product_structure +from . import test_product_template diff --git a/mrp_bom_attribute_match_semifinished_product/tests/test_finished_product_common.py b/mrp_bom_attribute_match_semifinished_product/tests/test_finished_product_common.py new file mode 100644 index 000000000..e0d07bc3b --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/tests/test_finished_product_common.py @@ -0,0 +1,32 @@ +from odoo.tests import Form, TransactionCase + + +class TestFinishedProductCommon(TransactionCase): + def setUp(self): + super(TestFinishedProductCommon, self).setUp() + + # Legs Attribute + self.legs_attribute = self.env.ref("product.product_attribute_1") + self.legs_steel_attr_value = self.env.ref("product.product_attribute_value_1") + self.legs_aluminium_attr_value = self.env.ref( + "product.product_attribute_value_2" + ) + + # Color Attribute + self.color_attribute = self.env.ref("product.product_attribute_2") + self.color_white_attr_value = self.env.ref("product.product_attribute_value_3") + self.color_black_attr_value = self.env.ref("product.product_attribute_value_4") + + # Create Valid Product + form = Form(self.env["product.template"]) + form.name = "Product #1" + form.finished_product = True + with form.attribute_line_ids.new() as attribute: + attribute.attribute_id = self.legs_attribute + attribute.value_ids.add(self.legs_steel_attr_value) + attribute.value_ids.add(self.legs_aluminium_attr_value) + with form.attribute_line_ids.new() as attribute: + attribute.attribute_id = self.color_attribute + attribute.value_ids.add(self.color_white_attr_value) + attribute.value_ids.add(self.color_black_attr_value) + self.product_1 = form.save() diff --git a/mrp_bom_attribute_match_semifinished_product/tests/test_finished_product_structure.py b/mrp_bom_attribute_match_semifinished_product/tests/test_finished_product_structure.py new file mode 100644 index 000000000..7bb194360 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/tests/test_finished_product_structure.py @@ -0,0 +1,249 @@ +from odoo.exceptions import MissingError, UserError +from odoo.tests import Form + +from .test_finished_product_common import TestFinishedProductCommon + + +class TestFinishedProductStructure(TestFinishedProductCommon): + def setUp(self): + super(TestFinishedProductStructure, self).setUp() + self.size_attr = self.env["product.attribute"].create({"name": "Size"}) + self.size_attr_value_s = self.env["product.attribute.value"].create( + {"name": "S", "attribute_id": self.size_attr.id} + ) + self.size_attr_value_m = self.env["product.attribute.value"].create( + {"name": "M", "attribute_id": self.size_attr.id} + ) + self.size_attr_value_l = self.env["product.attribute.value"].create( + {"name": "L", "attribute_id": self.size_attr.id} + ) + form = Form(self.env["product.template"]) + form.name = "Product #1" + form.finished_product = True + with form.attribute_line_ids.new() as attribute: + attribute.attribute_id = self.color_attribute + attribute.value_ids.add(self.color_white_attr_value) + attribute.value_ids.add(self.color_black_attr_value) + self.product_2 = form.save() + + form = Form(self.env["product.template"]) + form.name = "Product #1" + form.finished_product = True + with form.attribute_line_ids.new() as attribute: + attribute.attribute_id = self.legs_attribute + attribute.value_ids.add(self.legs_steel_attr_value) + attribute.value_ids.add(self.legs_aluminium_attr_value) + self.product_3 = form.save() + + self.stage_1_product = self.env["product.template"].create( + {"name": "Product #1 Stage #1"} + ) + self.stage_2_product = self.env["product.template"].create( + {"name": "Product #2 Stage #2"} + ) + + self.partner_subcontractor = self.env["res.partner"].create( + {"name": "Subcontractor #1"} + ) + self.product_shirt_template = self.env["product.template"].create( + { + "name": "Shirt", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.size_attr.id, + "value_ids": [(6, 0, [self.size_attr_value_l.id])], + }, + ) + ], + } + ) + + def test_structure_without_lines(self): + form = Form( + self.env["finished.product.structure.wizard"].with_context( + default_finished_product_id=self.product_1.id + ) + ) + wizard = form.save() + with self.assertRaises(MissingError): + wizard.create_product_struct() + + def test_structure_check_attributes(self): + with Form( + self.env["finished.product.structure.wizard"].with_context( + default_finished_product_id=self.product_1.id + ) + ) as form: + attribute_ids = form._values.get("attribute_ids")[0][2] + self.assertListEqual( + attribute_ids, + [self.legs_attribute.id, self.color_attribute.id], + msg="List attributes IDS must be the same", + ) + form.finished_product_id = self.product_2 + attribute_ids = form._values.get("attribute_ids")[0][2] + self.assertListEqual( + attribute_ids, + self.color_attribute.ids, + msg="List attributes IDS must be the same", + ) + form.finished_product_id = self.product_3 + attribute_ids = form._values.get("attribute_ids")[0][2] + self.assertListEqual( + attribute_ids, + self.legs_attribute.ids, + msg="List attributes IDS must be the same", + ) + with self.assertRaises(UserError), form.line_ids.new() as line: + line.stage_name = "Stage 1" + line.product_tmpl_id = self.product_shirt_template + line.bom_type = "normal" + + def test_tmp_product_struct_line(self): + wizard = self.env["finished.product.structure.wizard"].create( + {"finished_product_id": self.product_1.id} + ) + tmp_record = wizard._tmp_product_struct_line() + self.assertRecordValues( + tmp_record, + [ + { + "stage_name": "{} - Start".format(self.product_1.name), + "product_tmpl_id": self.product_1.id, + "product_tmpl_stage_id": self.product_1.id, + } + ], + ) + + def _create_product_struct(self, product_id): + form = Form( + self.env["finished.product.structure.wizard"].with_context( + default_finished_product_id=product_id + ) + ) + with form.line_ids.new() as line: + line.stage_name = "Stage #1" + line.product_tmpl_id = self.stage_1_product + line.bom_type = "normal" + with form.line_ids.new() as line: + line.stage_name = "Stage #2" + line.product_tmpl_id = self.stage_2_product + line.bom_type = "normal" + return form.save() + + def test_create_product_struct(self): + wizard = self._create_product_struct(self.product_1.id) + result = wizard.create_product_struct() + self.assertDictEqual( + result, + {"type": "ir.actions.act_window_close"}, + msg="Dicts must be the same", + ) + self.assertEqual( + len(self.product_1.semi_finished_product_tmpl_ids), + 2, + msg="Products count must be equal to 2", + ) + product_line_1, product_line_2 = self.product_1.semi_finished_product_tmpl_ids + self.assertEqual( + len(self.product_1.semi_finished_mrp_bom_ids), + 2, + msg="BOM's count must be equal to 2", + ) + bom_1, bom_2 = self.product_1.semi_finished_mrp_bom_ids + # Bom 1 + self.assertEqual( + bom_1.product_tmpl_id, + self.product_1, + msg="Bom product must be equal to ID #{}".format(self.product_1.id), + ) + line_ids = bom_1.bom_line_ids + self.assertEqual( + line_ids.component_template_id, + product_line_1.semi_finished_product_tmpl_id, + msg="Bom component product must be equal to ID #{}".format( + product_line_1.semi_finished_product_tmpl_id.id + ), + ) + self.assertListEqual( + line_ids.match_on_attribute_ids.ids, + [self.legs_attribute.id, self.color_attribute.id], + msg="Attributes must be the same", + ) + # Bom 2 + self.assertEqual( + bom_2.product_tmpl_id, + product_line_1.semi_finished_product_tmpl_id, + msg="Bom product must be equal to ID #{}".format( + product_line_1.semi_finished_product_tmpl_id.id + ), + ) + line_ids = bom_2.bom_line_ids + self.assertEqual( + line_ids.component_template_id, + product_line_2.semi_finished_product_tmpl_id, + msg="Bom component product must be equal to ID #{}".format( + product_line_2.semi_finished_product_tmpl_id.id + ), + ) + self.assertListEqual( + line_ids.match_on_attribute_ids.ids, + [self.legs_attribute.id, self.color_attribute.id], + msg="Attributes must be the same", + ) + + def test_recreate_product_struct(self): + wizard = self._create_product_struct(self.product_1.id) + wizard.create_product_struct() + semi_first_product_ids = self.product_1.semi_finished_product_tmpl_ids.ids + bom_first_ids = self.product_1.semi_finished_product_tmpl_ids.ids + # Recreate structure + wizard = self._create_product_struct(self.product_1.id) + wizard.create_product_struct() + semi_second_product_ids = self.product_1.semi_finished_product_tmpl_ids.ids + bom_second_ids = self.product_1.semi_finished_product_tmpl_ids.ids + self.assertNotEqual(semi_first_product_ids, semi_second_product_ids) + self.assertNotEqual(bom_first_ids, bom_second_ids) + + def test_prepare_bom_by_products(self): + form = Form( + self.env["finished.product.structure.wizard"].with_context( + default_finished_product_id=self.product_1 + ) + ) + with form.line_ids.new() as line: + line.stage_name = "Stage #1" + line.product_tmpl_id = self.stage_1_product + line.bom_type = "normal" + with form.line_ids.new() as line: + line.stage_name = "Stage #2" + line.product_tmpl_id = self.stage_2_product + line.bom_type = "subcontract" + line.partner_ids.add(self.partner_subcontractor) + wizard = form.save() + vals = wizard._prepare_bom_by_products() + bom_1_vals, bom_2_vals = vals + self.assertEqual( + bom_1_vals.get("product_tmpl_id"), + self.product_1.id, + msg="Product Template must be equal to ID #{}".format(self.product_1.id), + ) + self.assertEqual( + bom_1_vals.get("type"), "normal", msg="Bom type must be equal to normal" + ) + self.assertFalse( + bom_2_vals.get("product_tmpl_id"), msg="Product Template not set" + ) + self.assertEqual( + bom_2_vals.get("type"), + "subcontract", + msg="Bom type must be equal to 'subcontract'", + ) + self.assertEqual( + bom_2_vals.get("subcontractor_ids")[0][2], + self.partner_subcontractor.ids, + msg="Partner subcontractor must be contains in bom vals", + ) diff --git a/mrp_bom_attribute_match_semifinished_product/tests/test_product_template.py b/mrp_bom_attribute_match_semifinished_product/tests/test_product_template.py new file mode 100644 index 000000000..4809fe747 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/tests/test_product_template.py @@ -0,0 +1,52 @@ +from odoo.exceptions import UserError +from odoo.tests import Form + +from .test_finished_product_common import TestFinishedProductCommon + + +class TestProductTemplate(TestFinishedProductCommon): + def test_create_product_template_without_attributes(self): + """Test flow when finished product created without attributes""" + form = Form(self.env["product.template"]) + form.name = "Product #2" + form.finished_product = True + with self.assertRaises(UserError): + form.save() + + def test_edit_attributes_in_finished_product_template(self): + """Test flow when all attribute is removed from finished product""" + with self.assertRaises(UserError), Form(self.product_1) as form: + form.attribute_line_ids.remove(index=1) + form.attribute_line_ids.remove(index=0) + + def test_action_finished_product_structure_invalid(self): + """Test flow when action is raised error for not finished product""" + self.product_1.finished_product = False + with self.assertRaises(UserError): + self.product_1.action_finished_product_structure() + + def test_action_finished_product_structure_valid(self): + """Test flow when action is correct for finished product""" + action = self.product_1.action_finished_product_structure() + self.assertEqual( + action.get("type"), + "ir.actions.act_window", + msg="Action type must be equal to 'ir.actions.act_window'", + ) + self.assertEqual( + action.get("view_mode"), + "form", + msg="Action view mode must be equal to 'form'", + ) + self.assertEqual( + action.get("res_model"), + "finished.product.structure.wizard", + msg="Action res model must be equal to 'finished.product.structure.wizard'", + ) + self.assertEqual( + action.get("target"), "new", msg="Action target must be equal to 'new'" + ) + context = {"default_finished_product_id": self.product_1.id} + self.assertDictEqual( + action.get("context"), context, msg="Contexts must be the same" + ) diff --git a/mrp_bom_attribute_match_semifinished_product/views/product_template_views.xml b/mrp_bom_attribute_match_semifinished_product/views/product_template_views.xml new file mode 100644 index 000000000..1b23ca70b --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/views/product_template_views.xml @@ -0,0 +1,50 @@ + + + + + product.template_procurement.inherit.form.view + product.template + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mrp_bom_attribute_match_semifinished_product/views/semi_finished_product_template_line_views.xml b/mrp_bom_attribute_match_semifinished_product/views/semi_finished_product_template_line_views.xml new file mode 100644 index 000000000..2ae28c0f4 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/views/semi_finished_product_template_line_views.xml @@ -0,0 +1,25 @@ + + + + + semi.finished.product.template.line.view.form + semi.finished.product.template.line + +
+ + + + + + + + + +
+
+
+ +
diff --git a/mrp_bom_attribute_match_semifinished_product/wizard/__init__.py b/mrp_bom_attribute_match_semifinished_product/wizard/__init__.py new file mode 100644 index 000000000..063468719 --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import finished_product_structure_line +from . import finished_product_structure_wizard diff --git a/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_line.py b/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_line.py new file mode 100644 index 000000000..459596daa --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_line.py @@ -0,0 +1,56 @@ +from odoo import _, api, fields, models + + +class FinishedProductStructureLine(models.TransientModel): + _name = "finished.product.structure.line" + + structure_id = fields.Many2one("finished.product.structure.wizard") + stage_name = fields.Char(required=True) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + required=True, + string="Template product", + ) + valid_attribute_ids = fields.Many2many( + comodel_name="product.attribute", + relation="product_attribute_valid_rel", + ) + attribute_ids = fields.Many2many( + comodel_name="product.attribute", + relation="finished_product_struct_line_attribute_rel", + domain="[('id', 'in', valid_attribute_ids)]", + required=True, + string="Attribute(s)", + ) + bom_type = fields.Selection( + selection=[ + ("normal", "Manufacture this product"), + ("phantom", "Kit"), + ("subcontract", "Subcontracting"), + ], + default="normal", + required=True, + ) + partner_ids = fields.Many2many(comodel_name="res.partner", string="Subcontractor") + product_tmpl_stage_id = fields.Many2one(comodel_name="product.template") + + @api.onchange("product_tmpl_id") + def _onchange_product_tmpl_id(self): + self._check_product_tmpl_id() + + @api.constrains("product_tmpl_id") + def _check_product_tmpl_id(self): + for rec in self: + if rec.product_tmpl_id.attribute_line_ids: + raise models.ValidationError( + _( + "You can only add products without attributes as 'Template products'." + ) + ) + + @api.model + def default_get(self, fields): + result = super(FinishedProductStructureLine, self).default_get(fields) + if result.get("valid_attribute_ids"): + result["attribute_ids"] = result["valid_attribute_ids"] + return result diff --git a/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_wizard.py b/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_wizard.py new file mode 100644 index 000000000..31e1350fc --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_wizard.py @@ -0,0 +1,139 @@ +from itertools import tee + +from odoo import _, api, fields, models + + +def pairwise(iterable): + """s -> (s0,s1), (s1,s2), (s2, s3), ...""" + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +class FinishedProductStructureWizard(models.TransientModel): + _name = "finished.product.structure.wizard" + + finished_product_id = fields.Many2one( + comodel_name="product.template", + domain="[('finished_product', '=', True)]", + required=True, + ) + line_ids = fields.One2many( + comodel_name="finished.product.structure.line", inverse_name="structure_id" + ) + attribute_ids = fields.Many2many( + comodel_name="product.attribute", compute="_compute_attribute_ids", store=True + ) + need_confirmation = fields.Boolean() + + @api.onchange("finished_product_id") + def _onchange_finished_product_id(self): + """ + Update attribute and line attributes + by onchange finished product + """ + self.line_ids.write({"attribute_ids": [(6, 0, self.attribute_ids.ids)]}) + self.need_confirmation = bool( + self.finished_product_id.semi_finished_mrp_bom_ids + ) + + @api.depends("finished_product_id") + def _compute_attribute_ids(self): + for rec in self: + rec.attribute_ids = rec.finished_product_id.attribute_line_ids.mapped( + "attribute_id" + ) + + def _prepare_new_product_templates(self): + finished_product = self.finished_product_id + for line in self.line_ids: + new_product_name = "{} - {}".format(finished_product.name, line.stage_name) + product_tmpl = line.product_tmpl_id.copy({"name": new_product_name}) + attr_lines = finished_product.attribute_line_ids.filtered( + lambda pl: pl.attribute_id in line.attribute_ids + ) + for attr_line in attr_lines: + attr_line.copy({"product_tmpl_id": product_tmpl.id}) + line.product_tmpl_stage_id = product_tmpl + + def _tmp_product_struct_line(self): + self.ensure_one() + return self.env["finished.product.structure.line"].new( + { + "stage_name": "{} - Start".format(self.finished_product_id.name), + "product_tmpl_id": self.finished_product_id, + "product_tmpl_stage_id": self.finished_product_id, + } + ) + + def _prepare_bom_by_products(self): + lines = self._tmp_product_struct_line() | self.line_ids + vals_list = [] + for bom, line in pairwise(lines): + vals = { + "product_tmpl_id": bom.product_tmpl_stage_id.id, + "type": line.bom_type, + "bom_line_ids": [ + ( + 0, + 0, + { + "component_template_id": line.product_tmpl_stage_id.id, + "match_on_attribute_ids": [(6, 0, line.attribute_ids.ids)], + }, + ) + ], + } + if line.bom_type == "subcontract": + vals.update(subcontractor_ids=[(6, 0, line.partner_ids.ids)]) + self.finished_product_id.write( + { + "semi_finished_product_tmpl_ids": [ + ( + 0, + 0, + { + "semi_finished_product_tmpl_id": line.product_tmpl_stage_id.id, + "attribute_ids": [(6, 0, line.attribute_ids.ids)], + "bom_type": line.bom_type, + "partner_ids": [(6, 0, line.partner_ids.ids)], + }, + ) + ] + } + ) + vals_list.append(vals) + return vals_list + + def remove_old_struct(self): + boms = self.finished_product_id.semi_finished_mrp_bom_ids + if boms: + archive_bom_ids = ( + self.env["mrp.production"] + .search( + [ + ("bom_id", "in", boms.ids), + ("state", "not in", ["done", "cancel"]), + ] + ) + .mapped("bom_id") + ) + (boms - archive_bom_ids).unlink() + archive_bom_ids.write({"active": False}) + lines = self.finished_product_id.semi_finished_product_tmpl_ids + if lines: + products = lines.mapped("semi_finished_product_tmpl_id") + lines.unlink() + products.unlink() + + def create_product_struct(self): + if not self.line_ids: + raise models.MissingError( + _("At least one line needs to be added to structure.") + ) + self.remove_old_struct() + self._prepare_new_product_templates() + vals_list = self._prepare_bom_by_products() + bom_ids = self.env["mrp.bom"].create(vals_list) + self.finished_product_id.semi_finished_mrp_bom_ids = bom_ids + return {"type": "ir.actions.act_window_close"} diff --git a/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_wizard.xml b/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_wizard.xml new file mode 100644 index 000000000..f5b8b09cd --- /dev/null +++ b/mrp_bom_attribute_match_semifinished_product/wizard/finished_product_structure_wizard.xml @@ -0,0 +1,73 @@ + + + + + finished.product.structure.wizard.form.view + finished.product.structure.wizard + +
+ + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + Create finished product structure + + + + form + code + + if records: + action = records.action_finished_product_structure() + + + +
diff --git a/setup/mrp_bom_attribute_match_semifinished_product/odoo/addons/mrp_bom_attribute_match_semifinished_product b/setup/mrp_bom_attribute_match_semifinished_product/odoo/addons/mrp_bom_attribute_match_semifinished_product new file mode 120000 index 000000000..f81ae5bf2 --- /dev/null +++ b/setup/mrp_bom_attribute_match_semifinished_product/odoo/addons/mrp_bom_attribute_match_semifinished_product @@ -0,0 +1 @@ +../../../../mrp_bom_attribute_match_semifinished_product \ No newline at end of file diff --git a/setup/mrp_bom_attribute_match_semifinished_product/setup.py b/setup/mrp_bom_attribute_match_semifinished_product/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/mrp_bom_attribute_match_semifinished_product/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)