Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions stock_product_variant_mto/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Stock Product Variant MTO
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.
Expand Down Expand Up @@ -64,6 +66,15 @@ Contributors

- Matthieu Méquignon <[email protected]>
- Akim Juillerat <[email protected]>
- Chau Le <[email protected]>

Other credits
-------------

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

- Camptocamp

Maintainers
-----------
Expand Down
2 changes: 1 addition & 1 deletion stock_product_variant_mto/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"name": "Stock Product Variant MTO",
"summary": "Allow to individually set variants as MTO",
"version": "14.0.1.0.0",
"version": "18.0.1.0.0",
"development_status": "Alpha",
"category": "Inventory",
"website": "https://github.com/OCA/stock-logistics-workflow",
Expand Down
48 changes: 34 additions & 14 deletions stock_product_variant_mto/models/product_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,50 @@ class ProductProduct(models.Model):
)

route_ids = fields.Many2many(
"stock.location.route",
"stock.route",
compute="_compute_route_ids",
domain="[('product_selectable', '=', True)]",
store=False,
domain="[('product_selectable', '=', True)]",
search="_search_route_ids",
)

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:
if not mto_route:
product.is_mto = False
continue
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:
if mto_route and mto_route in product.product_tmpl_id.route_ids:
if not product.is_mto:
product.route_ids = product.product_tmpl_id.route_ids - mto_route
continue
else:
if mto_route and product.is_mto:
product.route_ids = product.product_tmpl_id.route_ids + mto_route
continue
product.route_ids = product.product_tmpl_id.route_ids
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

def _search_route_ids(self, operator, value):
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False)
if not mto_route:
return [(0, "=", 1)] if operator in ("=", "in") else [(0, "=", 0)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ you need to check the value before returning such a domain.

If I search for something like [('route_ids, in, buy_route.id)]` I would expect to get the products where the buy route is defined and the MTO being archived or not should not have any influence.


if (operator, value) in [("=", mto_route.id), ("in", [mto_route.id])]:
return [
("product_tmpl_id.route_ids", operator, value),
("is_mto", "=", True),
]
elif operator in ("!=", "not in") and mto_route.id in value:
return [
"|",
("product_tmpl_id.route_ids", operator, value),
("is_mto", "=", False),
]
return [("product_tmpl_id.route_ids", operator, value)]
56 changes: 34 additions & 22 deletions stock_product_variant_mto/models/product_template.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,70 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo import _, api, models
from odoo import api, models


class ProductTemplate(models.Model):
_inherit = "product.template"

def write(self, values):
if "route_ids" not in 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
mto_route = self.env.ref("stock.route_warehouse0_mto")
template_not_mto_before = self.filtered(lambda t: mto_route not in t.route_ids)

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 = template_not_mto_before & templates_mto_after
templates_mto_removed = (self - template_not_mto_before) & (
self - templates_mto_after
)
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 (
mto_route not in self._origin.route_ids
and mto_route in self.route_ids._origin
):
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": _("Warning"),
"message": _(
"Activating MTO route will reset `Variant is MTO` setting on the variants."
"title": self.env._("Warning"),
"message": self.env._(
"Activating MTO route will reset `Variant is MTO` "
"setting on the variants."
),
}
}
if (
mto_route in self._origin.route_ids
and mto_route not in self.route_ids._origin
):

if mto_route in origin_routes and mto_route not in current_routes:
# Return warning deactivating MTO route
return {
"warning": {
"title": _("Warning"),
"message": _(
"Deactivating MTO route will reset `Variant is MTO` setting on the variants."
"title": self.env._("Warning"),
"message": self.env._(
"Deactivating MTO route will reset `Variant is MTO` "
"setting on the variants."
),
}
}
1 change: 1 addition & 0 deletions stock_product_variant_mto/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- Matthieu Méquignon \<<[email protected]>\>
- Akim Juillerat \<<[email protected]>\>
- Chau Le \<<[email protected]>\>
3 changes: 3 additions & 0 deletions stock_product_variant_mto/readme/CREDITS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The development and migration of this module has been financially supported by:

- Camptocamp
2 changes: 2 additions & 0 deletions stock_product_variant_mto/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,2 +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.
15 changes: 13 additions & 2 deletions stock_product_variant_mto/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ <h1 class="title">Stock Product Variant MTO</h1>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/stock-logistics-workflow/tree/18.0/stock_product_variant_mto"><img alt="OCA/stock-logistics-workflow" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-workflow-18-0/stock-logistics-workflow-18-0-stock_product_variant_mto"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to define if a product variant can use the Make To
Order route without any dependency on its template routes settings.</p>
<p>The routes are moved from product template to product product.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Expand All @@ -385,7 +386,8 @@ <h1 class="title">Stock Product Variant MTO</h1>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-5">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
Expand All @@ -411,10 +413,19 @@ <h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<ul class="simple">
<li>Matthieu Méquignon &lt;<a class="reference external" href="mailto:matthieu.mequignon&#64;camptocamp.com">matthieu.mequignon&#64;camptocamp.com</a>&gt;</li>
<li>Akim Juillerat &lt;<a class="reference external" href="mailto:akim.juillerat&#64;camptocamp.com">akim.juillerat&#64;camptocamp.com</a>&gt;</li>
<li>Chau Le &lt;<a class="reference external" href="mailto:chaulb&#64;trobz.com">chaulb&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-5">Other credits</a></h2>
<p>The development and migration of this module has been financially
supported by:</p>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
Expand Down
13 changes: 6 additions & 7 deletions stock_product_variant_mto/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo.tests.common import Form, SavepointCase
from odoo.tests import Form, tagged

from odoo.addons.base.tests.common import BaseCommon

class TestMTOVariantCommon(SavepointCase):
at_install = False
post_install = True

@tagged("post_install", "-at_install")
class TestMTOVariantCommon(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.setUpClassProduct()

@classmethod
Expand Down Expand Up @@ -79,13 +78,13 @@ def toggle_is_mto(self, records):
record.is_mto = not record.is_mto

def assertVariantsMTO(self, records):
records.invalidate_cache(["is_mto"])
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_cache(["is_mto"])
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)
47 changes: 30 additions & 17 deletions stock_product_variant_mto/tests/test_mto_variant.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

from .common import TestMTOVariantCommon

onchange_logger = "odoo.tests.form.onchange"

_logger = logging.getLogger(onchange_logger)


class TestMTOVariant(TestMTOVariantCommon):
def test_variants_mto(self):
Expand All @@ -24,14 +28,16 @@ def test_variants_mto(self):
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
self.add_route(pen_template, self.mto_route)
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
self.remove_route(pen_template, self.mto_route)
with self.assertLogs(onchange_logger, level="WARNING"):
self.remove_route(pen_template, self.mto_route)
self.assertVariantsNotMTO(pens)

def test_template_routes_updated(self):
Expand All @@ -44,7 +50,8 @@ def test_template_routes_updated(self):
black_pen = self.black_pen
self.assertVariantsNotMTO(pens)
# If template is set to MTO, all variants are updated
self.add_route(pen_template, self.mto_route)
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)
Expand All @@ -65,43 +72,49 @@ def test_template_warnings(self):
red_pen = self.red_pen
green_pen = self.green_pen
black_pen = self.black_pen
onchange_logger = logging.getLogger("odoo.tests.common.onchange")
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) as log:
with self.assertLogs(onchange_logger, level="WARNING") as log_catcher:
self.add_route(pen_template, self.mto_route)
self.assertIn("WARNING", log.output[0])
self.assertIn("Activating MTO route will reset", log.output[0])
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:
with self.assertLogs(onchange_logger) as log_catcher:
self.add_route(pen_template, random_route)
onchange_logger.info("No warning raised")
self.assertNotIn("WARNING", log.output[0])
_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:

# 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.output[0])
self.assertIn("Deactivating MTO route will reset", log.output[0])
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:
with self.assertLogs(onchange_logger) as log_catcher:
self.remove_route(pen_template, random_route)
onchange_logger.info("No warning raised")
self.assertNotIn("WARNING", log.output[0])
_logger.info("No warning raised")
self.assertVariantsMTO(black_pen)
self.assertVariantsNotMTO(blue_pen | green_pen | red_pen)