diff --git a/stock_product_variant_mto/README.rst b/stock_product_variant_mto/README.rst new file mode 100644 index 000000000000..bf66dd0e5bac --- /dev/null +++ b/stock_product_variant_mto/README.rst @@ -0,0 +1,102 @@ +========================= +Stock Product Variant MTO +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:19127c85672968523ba9dac247eafabed27aaff202e5e8cc20d87b1f6ee204c6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/18.0/stock_product_variant_mto + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-18-0/stock-logistics-workflow-18-0-stock_product_variant_mto + :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/stock-logistics-workflow&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to define if a product variant can use the Make To +Order route without any dependency on its template routes settings. + +The routes are moved from product template to product product. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +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 +------- + +* Camptocamp SA + +Contributors +------------ + +- Matthieu Méquignon +- Akim Juillerat +- Chau Le + +Other credits +------------- + +The development and migration of this module has been financially +supported by: + +- Camptocamp + +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-mmequignon| image:: https://github.com/mmequignon.png?size=40px + :target: https://github.com/mmequignon + :alt: mmequignon + +Current `maintainer `__: + +|maintainer-mmequignon| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_product_variant_mto/__init__.py b/stock_product_variant_mto/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_product_variant_mto/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_product_variant_mto/__manifest__.py b/stock_product_variant_mto/__manifest__.py new file mode 100644 index 000000000000..698a5c1dbed0 --- /dev/null +++ b/stock_product_variant_mto/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Stock Product Variant MTO", + "summary": "Allow to individually set variants as MTO", + "version": "18.0.1.0.0", + "development_status": "Alpha", + "category": "Inventory", + "website": "https://github.com/OCA/stock-logistics-workflow", + "author": "Camptocamp SA, Odoo Community Association (OCA)", + "maintainers": ["mmequignon"], + "license": "AGPL-3", + "installable": True, + "auto_install": False, + "depends": ["stock"], + "data": ["views/product_product.xml"], +} diff --git a/stock_product_variant_mto/models/__init__.py b/stock_product_variant_mto/models/__init__.py new file mode 100644 index 000000000000..18b37e853203 --- /dev/null +++ b/stock_product_variant_mto/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_product +from . import product_template diff --git a/stock_product_variant_mto/models/product_product.py b/stock_product_variant_mto/models/product_product.py new file mode 100644 index 000000000000..bfe7666e5a78 --- /dev/null +++ b/stock_product_variant_mto/models/product_product.py @@ -0,0 +1,53 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + +IS_MTO_HELP = """ + Check or Uncheck this field to enable the Make To Order on the variant, + independantly from its template configuration.\n + Please note that activating or deactivating Make To Order on the template, + will reset this setting on its variants. +""" + + +class ProductProduct(models.Model): + _inherit = "product.product" + + is_mto = fields.Boolean( + string="Variant is MTO", + compute="_compute_is_mto", + store=True, + readonly=False, + help=IS_MTO_HELP, + ) + + route_ids = fields.Many2many( + "stock.route", + compute="_compute_route_ids", + domain="[('product_selectable', '=', True)]", + store=True, + ) + + def _compute_is_mto(self): + mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) + if not mto_route: + self.update({"is_mto": False}) + return + + for product in self: + product.is_mto = mto_route in product.product_tmpl_id.route_ids + + @api.depends("is_mto", "product_tmpl_id.route_ids") + def _compute_route_ids(self): + mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) + for product in self: + template_routes = product.product_tmpl_id.route_ids + + if mto_route: + if product.is_mto and mto_route not in template_routes: + template_routes += mto_route + elif not product.is_mto and mto_route in template_routes: + template_routes -= mto_route + + product.route_ids = template_routes diff --git a/stock_product_variant_mto/models/product_template.py b/stock_product_variant_mto/models/product_template.py new file mode 100644 index 000000000000..d69c3335ac0a --- /dev/null +++ b/stock_product_variant_mto/models/product_template.py @@ -0,0 +1,70 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def write(self, values): + mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) + + if "route_ids" not in values or not mto_route: + return super().write(values) + + # As _compute_is_mto cannot use api.depends (or it would reset MTO + # route on variants as soon as there is a change on the template routes), + # we need to check which template in self had MTO route activated + # or deactivated to force the recomputation of is_mto on variants + + templates_not_mto_before = self.filtered(lambda t: mto_route not in t.route_ids) + + res = super().write(values) + + templates_mto_after = self.filtered(lambda t: mto_route in t.route_ids) + templates_mto_added = templates_not_mto_before & templates_mto_after + templates_mto_removed = self - templates_mto_after - templates_not_mto_before + + ( + templates_mto_added | templates_mto_removed + ).product_variant_ids._compute_is_mto() + + return res + + @api.onchange("route_ids") + def onchange_route_ids(self): + mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) + if not mto_route: + return + + origin_routes = ( + self._origin.route_ids if self._origin else self.env["stock.route"] + ) + current_routes = ( + self.route_ids._origin if self.route_ids else self.env["stock.route"] + ) + + if mto_route not in origin_routes and mto_route in current_routes: + # Return warning activating MTO route + return { + "warning": { + "title": self.env._("Warning"), + "message": self.env._( + "Activating MTO route will reset `Variant is MTO` " + "setting on the variants." + ), + } + } + + if mto_route in origin_routes and mto_route not in current_routes: + # Return warning deactivating MTO route + return { + "warning": { + "title": self.env._("Warning"), + "message": self.env._( + "Deactivating MTO route will reset `Variant is MTO` " + "setting on the variants." + ), + } + } diff --git a/stock_product_variant_mto/pyproject.toml b/stock_product_variant_mto/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/stock_product_variant_mto/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/stock_product_variant_mto/readme/CONFIGURATION.md b/stock_product_variant_mto/readme/CONFIGURATION.md new file mode 100644 index 000000000000..616f736ea524 --- /dev/null +++ b/stock_product_variant_mto/readme/CONFIGURATION.md @@ -0,0 +1,2 @@ +The checkbox `Variant is MTO` on the product variant allows +to force usage or non-usage of the MTO route for the variant. diff --git a/stock_product_variant_mto/readme/CONTRIBUTORS.md b/stock_product_variant_mto/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..a5082bc02f1a --- /dev/null +++ b/stock_product_variant_mto/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Matthieu Méquignon \<\> +- Akim Juillerat \<\> +- Chau Le \<\> diff --git a/stock_product_variant_mto/readme/CREDITS.md b/stock_product_variant_mto/readme/CREDITS.md new file mode 100644 index 000000000000..c2d2a1e6b83c --- /dev/null +++ b/stock_product_variant_mto/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development and migration of this module has been financially supported by: + +- Camptocamp diff --git a/stock_product_variant_mto/readme/DESCRIPTION.md b/stock_product_variant_mto/readme/DESCRIPTION.md new file mode 100644 index 000000000000..59c47d0386b0 --- /dev/null +++ b/stock_product_variant_mto/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This module allows to define if a product variant can use the Make To +Order route without any dependency on its template routes settings. + +The routes are moved from product template to product product. diff --git a/stock_product_variant_mto/static/description/index.html b/stock_product_variant_mto/static/description/index.html new file mode 100644 index 000000000000..afc051658b61 --- /dev/null +++ b/stock_product_variant_mto/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +Stock Product Variant MTO + + + +
+

Stock Product Variant MTO

+ + +

Alpha License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

This module allows to define if a product variant can use the Make To +Order route without any dependency on its template routes settings.

+

The routes are moved from product template to product product.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

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

+
    +
  • Camptocamp SA
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development and migration of this module has been financially +supported by:

+
    +
  • Camptocamp
  • +
+
+
+

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 maintainer:

+

mmequignon

+

This module is part of the OCA/stock-logistics-workflow project on GitHub.

+

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

+
+
+
+ + diff --git a/stock_product_variant_mto/tests/__init__.py b/stock_product_variant_mto/tests/__init__.py new file mode 100644 index 000000000000..8cc9739069e5 --- /dev/null +++ b/stock_product_variant_mto/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mto_variant diff --git a/stock_product_variant_mto/tests/common.py b/stock_product_variant_mto/tests/common.py new file mode 100644 index 000000000000..e5188da3b834 --- /dev/null +++ b/stock_product_variant_mto/tests/common.py @@ -0,0 +1,90 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests import Form, tagged + +from odoo.addons.base.tests.common import BaseCommon + + +@tagged("post_install", "-at_install") +class TestMTOVariantCommon(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.setUpClassProduct() + + @classmethod + def setUpClassProduct(cls): + cls.color = cls.env["product.attribute"].create({"name": "Color"}) + value_model = cls.env["product.attribute.value"] + cls.values = value_model.create( + [ + {"name": "red", "attribute_id": cls.color.id}, + {"name": "blue", "attribute_id": cls.color.id}, + {"name": "black", "attribute_id": cls.color.id}, + {"name": "green", "attribute_id": cls.color.id}, + ] + ) + cls.value_red = cls.values.filtered(lambda v: v.name == "red") + cls.value_blue = cls.values.filtered(lambda v: v.name == "blue") + cls.value_black = cls.values.filtered(lambda v: v.name == "black") + cls.value_green = cls.values.filtered(lambda v: v.name == "green") + cls.template_pen = cls.env["product.template"].create( + { + "name": "pen", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.color.id, + "value_ids": [(6, 0, cls.values.ids)], + }, + ) + ], + } + ) + cls.variants_pen = cls.template_pen.product_variant_ids + cls.black_pen = cls.variants_pen.filtered( + lambda v: v.product_template_attribute_value_ids.name == "black" + ) + cls.green_pen = cls.variants_pen.filtered( + lambda v: v.product_template_attribute_value_ids.name == "green" + ) + cls.red_pen = cls.variants_pen.filtered( + lambda v: v.product_template_attribute_value_ids.name == "red" + ) + cls.blue_pen = cls.variants_pen.filtered( + lambda v: v.product_template_attribute_value_ids.name == "blue" + ) + cls.mto_route = cls.env.ref("stock.route_warehouse0_mto") + cls.mto_route.active = True + + def add_route(self, template, route): + if not route: + route = self.mto_route + with Form(template) as record: + record.route_ids.add(route) + + def remove_route(self, template, route): + if not route: + route = self.mto_route + with Form(template) as record: + record.route_ids.remove(id=route.id) + + @classmethod + def toggle_is_mto(self, records): + for record in records: + record.is_mto = not record.is_mto + + def assertVariantsMTO(self, records): + records.invalidate_recordset(["is_mto"]) + self.assertTrue(all([record.is_mto for record in records])) + for rec in records: + self.assertIn(self.mto_route, rec.route_ids) + + def assertVariantsNotMTO(self, records): + records.invalidate_recordset(["is_mto"]) + self.assertFalse(any([record.is_mto for record in records])) + for rec in records: + self.assertNotIn(self.mto_route, rec.route_ids) diff --git a/stock_product_variant_mto/tests/test_mto_variant.py b/stock_product_variant_mto/tests/test_mto_variant.py new file mode 100644 index 000000000000..4563b108fe53 --- /dev/null +++ b/stock_product_variant_mto/tests/test_mto_variant.py @@ -0,0 +1,120 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import logging + +from .common import TestMTOVariantCommon + +onchange_logger = "odoo.tests.form.onchange" + +_logger = logging.getLogger(onchange_logger) + + +class TestMTOVariant(TestMTOVariantCommon): + def test_variants_mto(self): + # instanciate variables + pen_template = self.template_pen + pens = self.variants_pen + blue_pen = self.blue_pen + red_pen = self.red_pen + green_pen = self.green_pen + black_pen = self.black_pen + self.assertVariantsNotMTO(pens) + # enable mto route for black pen + self.toggle_is_mto(black_pen) + self.assertVariantsMTO(black_pen) + self.assertVariantsNotMTO(blue_pen | green_pen | red_pen) + # enable mto route for black and blue pens + self.toggle_is_mto(blue_pen) + self.assertVariantsMTO(black_pen | blue_pen) + self.assertVariantsNotMTO(red_pen | green_pen) + # Now enable the mto route for the template, all variants get is_mto = True + with self.assertLogs(onchange_logger, level="WARNING"): + self.add_route(pen_template, self.mto_route) + self.assertVariantsMTO(pens) + # Disable mto route for black_pen + self.toggle_is_mto(black_pen) + self.assertVariantsNotMTO(black_pen) + self.assertVariantsMTO(blue_pen | green_pen | red_pen) + # Disable mto route on the template, reset is_mto on variants + with self.assertLogs(onchange_logger, level="WARNING"): + self.remove_route(pen_template, self.mto_route) + self.assertVariantsNotMTO(pens) + + def test_template_routes_updated(self): + # instanciate variables + pen_template = self.template_pen + pens = self.variants_pen + blue_pen = self.blue_pen + red_pen = self.red_pen + green_pen = self.green_pen + black_pen = self.black_pen + self.assertVariantsNotMTO(pens) + # If template is set to MTO, all variants are updated + with self.assertLogs(onchange_logger, level="WARNING"): + self.add_route(pen_template, self.mto_route) + self.assertVariantsMTO(pens) + # Now toggle a few variants to is_mto == False + self.toggle_is_mto(black_pen | blue_pen) + self.assertVariantsMTO(green_pen | red_pen) + self.assertVariantsNotMTO(black_pen | blue_pen) + # Now modifying template.route_ids to trigger variant's _compute_is_mto + random_route = self.mto_route.create({"name": "loutourout de la vit"}) + self.add_route(pen_template, random_route) + # Template is still MTO, but variants is_mto shouldn't have changed + self.assertVariantsMTO(green_pen | red_pen) + self.assertVariantsNotMTO(black_pen | blue_pen) + + def test_template_warnings(self): + # instanciate variables + pen_template = self.template_pen + pens = self.variants_pen + blue_pen = self.blue_pen + red_pen = self.red_pen + green_pen = self.green_pen + black_pen = self.black_pen + self.assertVariantsNotMTO(pens) + + # enable mto route for black pen + self.toggle_is_mto(black_pen) + self.assertVariantsMTO(black_pen) + + # Enable mto route on the template, raise warning as is_mto is reset on variants + with self.assertLogs(onchange_logger, level="WARNING") as log_catcher: + self.add_route(pen_template, self.mto_route) + self.assertIn("WARNING", log_catcher.output[0]) + self.assertIn("Activating MTO route will reset", log_catcher.output[0]) + self.assertVariantsMTO(pens) + + # Disable mto route for black pen + self.toggle_is_mto(black_pen) + self.assertVariantsNotMTO(black_pen) + self.assertVariantsMTO(blue_pen | green_pen | red_pen) + + # Enable unrelated route does not raise warning nor reset + random_route = self.mto_route.create({"name": "loutourout de la vit"}) + with self.assertLogs(onchange_logger) as log_catcher: + self.add_route(pen_template, random_route) + _logger.info("No warning raised") + self.assertNotIn("WARNING", log_catcher.output[0]) + self.assertVariantsNotMTO(black_pen) + self.assertVariantsMTO(blue_pen | green_pen | red_pen) + + # Disable mto route on the template, + # raise warning as is_mto is reset on variants + with self.assertLogs(onchange_logger) as log_catcher: + self.remove_route(pen_template, self.mto_route) + self.assertIn("WARNING", log_catcher.output[0]) + self.assertIn("Deactivating MTO route will reset", log_catcher.output[0]) + self.assertVariantsNotMTO(pens) + + # Enable mto route for black pen + self.toggle_is_mto(black_pen) + self.assertVariantsMTO(black_pen) + self.assertVariantsNotMTO(blue_pen | green_pen | red_pen) + + # Disable unrelated route does not raise warning nor reset + with self.assertLogs(onchange_logger) as log_catcher: + self.remove_route(pen_template, random_route) + _logger.info("No warning raised") + self.assertVariantsMTO(black_pen) + self.assertVariantsNotMTO(blue_pen | green_pen | red_pen) diff --git a/stock_product_variant_mto/views/product_product.xml b/stock_product_variant_mto/views/product_product.xml new file mode 100644 index 000000000000..100ed6e03c47 --- /dev/null +++ b/stock_product_variant_mto/views/product_product.xml @@ -0,0 +1,26 @@ + + + + + product.product.form.inherit + product.product + + + + + + + + + + product.product.form.easy.inherit + product.product + + + + + + + +