Skip to content

Commit fd5bfac

Browse files
committed
Refactor mto_route_product_variant
Define route_ids as computed on the product variant, in order to reuse route_ids from its template and add or remove MTO route according to the setting on the variant. In case MTO route is changed on the template, it must reset any variant specific setting.
1 parent e694809 commit fd5bfac

File tree

8 files changed

+119
-60
lines changed

8 files changed

+119
-60
lines changed

stock_product_variant_mto/models/product_product.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
IS_MTO_HELP = """
77
Check or Uncheck this field to enable the Make To Order on the variant,
8-
independantly from its template configuration.
8+
independantly from its template configuration.\n
9+
Please note that activating or deactivating Make To Order on the template,
10+
will reset this setting on its variants.
911
"""
1012

1113
class ProductProduct(models.Model):
@@ -14,49 +16,35 @@ class ProductProduct(models.Model):
1416
is_mto = fields.Boolean(
1517
string="Variant is MTO",
1618
compute="_compute_is_mto",
17-
inverse="_inverse_is_mto",
1819
store=True,
20+
readonly=False,
1921
help=IS_MTO_HELP,
2022
)
2123

22-
@api.depends("product_tmpl_id.route_ids")
24+
route_ids = fields.Many2many(
25+
"stock.location.route",
26+
compute="_compute_route_ids",
27+
store=False
28+
)
29+
2330
def _compute_is_mto(self):
24-
# We only want to force all variants `is_mto` to True when
25-
# the mto route is explicitely set on its template.
26-
# Watching the mto route is not enough, since we might
27-
# have a template with the mto route, and disabled the mto route
28-
# for a few of its variants
29-
# If a user sets another route on the variant, we do not want the
30-
# mto disabled variants to be updated.
31-
# To ensure that, the created `has_mto_route_changed` boolean field
32-
# is set to True only when the MTO route is set on a template.
3331
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False)
34-
templates = self.product_tmpl_id
35-
# Only variants with a template with the MTO route and has_mto_route_changed
36-
# should be updated.
37-
mto_templates = templates.filtered(
38-
lambda t: mto_route in t.route_ids and t.has_mto_route_changed
39-
)
40-
mto_variants = self.filtered(lambda p: p.product_tmpl_id in mto_templates)
41-
mto_variants.is_mto = True
42-
# For the other variants, keep their current value.
43-
other_variants = self - mto_variants
44-
for variant in other_variants:
45-
variant.is_mto = variant.is_mto
46-
# Then set template's has_mto_route_changed to False, as it
47-
# has been handled above
48-
templates.has_mto_route_changed = False
49-
50-
def _inverse_is_mto(self):
51-
# When all variants of a template are `is_mto == False`, drop the MTO route
52-
# from the template, otherwise do nothing
32+
for product in self:
33+
if not mto_route:
34+
product.is_mto = False
35+
continue
36+
product.is_mto = mto_route in product.product_tmpl_id.route_ids
37+
38+
@api.depends("is_mto", "product_tmpl_id.route_ids")
39+
def _compute_route_ids(self):
5340
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False)
54-
for template in self.product_tmpl_id:
55-
is_mto = False
56-
for variant in template.product_variant_ids:
57-
if variant.is_mto:
58-
is_mto = True
59-
break
60-
# If no variant is mto, then drop the route of the template.
61-
if not is_mto:
62-
template.route_ids = [(3, mto_route.id, 0)]
41+
for product in self:
42+
if mto_route and mto_route in product.product_tmpl_id.route_ids:
43+
if not product.is_mto:
44+
product.route_ids = product.product_tmpl_id.route_ids - mto_route
45+
continue
46+
else:
47+
if mto_route and product.is_mto:
48+
product.route_ids = product.product_tmpl_id.route_ids + mto_route
49+
continue
50+
product.route_ids = product.product_tmpl_id.route_ids
Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
# Copyright 2023 Camptocamp SA
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
33

4-
from odoo import models, fields
4+
from odoo import _, api, models, fields
55

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

9-
has_mto_route_changed = fields.Boolean()
10-
119
def write(self, values):
1210
if not "route_ids" in values:
1311
return super().write(values)
14-
# This route_ids change will trigger variant's _compute_is_mto.
15-
# We want to set variant's is_mto to True only if their
16-
# template has been set to True here ↓
12+
# As _compute_is_mto cannot use api.depends (or it would reset MTO
13+
# route on variants as soon as there is a change on the template routes),
14+
# we need to check which template in self had MTO route activated
15+
# or deactivated to force the recomputation of is_mto on variants
1716
mto_route = self.env.ref("stock.route_warehouse0_mto")
1817
template_not_mto_before = self.filtered(lambda t: mto_route not in t.route_ids)
1918
res = super().write(values)
20-
template_mto_after = self.filtered(lambda t: mto_route in t.route_ids)
21-
# Templates where mto route has changed are those where
22-
# the mto route has been set
23-
templates_mto_set = template_not_mto_before & template_mto_after
24-
templates_mto_set.has_mto_route_changed = True
19+
templates_mto_after = self.filtered(lambda t: mto_route in t.route_ids)
20+
templates_mto_added = template_not_mto_before & templates_mto_after
21+
templates_mto_removed = (self - template_not_mto_before) & (self - templates_mto_after)
22+
(templates_mto_added | templates_mto_removed).product_variant_ids._compute_is_mto()
23+
24+
@api.onchange("route_ids")
25+
def onchange_route_ids(self):
26+
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False)
27+
if mto_route not in self._origin.route_ids and mto_route in self.route_ids._origin:
28+
# Return warning activating MTO route
29+
return {
30+
"warning": {
31+
"title": _("Warning"),
32+
"message": _("Activating MTO route will reset `Variant is MTO` setting on the variants.")
33+
}
34+
}
35+
if mto_route in self._origin.route_ids and mto_route not in self.route_ids._origin:
36+
# Return warning deactivating MTO route
37+
return {
38+
"warning": {
39+
"title": _("Warning"),
40+
"message": _("Deactivating MTO route will reset `Variant is MTO` setting on the variants.")
41+
}
42+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The checkbox `Variant is MTO` on the product variant allows
2+
to force usage or non-usage of the MTO route for the variant.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
* Matthieu Méquignon <[email protected]>
2+
* Akim Juillerat <[email protected]>
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
Allow to individually set variants as Make To Order
1+
This module allows to define if a product variant can use the
2+
Make To Order route without any dependency on its template routes
3+
settings.

stock_product_variant_mto/readme/USAGE.rst

Lines changed: 0 additions & 4 deletions
This file was deleted.

stock_product_variant_mto/tests/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ def toggle_is_mto(self, records):
7070
def assertVariantsMTO(self, records):
7171
records.invalidate_cache(["is_mto"])
7272
self.assertTrue(all([record.is_mto for record in records]))
73+
for rec in records:
74+
self.assertIn(self.mto_route, rec.route_ids)
7375

7476
def assertVariantsNotMTO(self, records):
7577
records.invalidate_cache(["is_mto"])
7678
self.assertFalse(any([record.is_mto for record in records]))
79+
for rec in records:
80+
self.assertNotIn(self.mto_route, rec.route_ids)

stock_product_variant_mto/tests/test_mto_variant.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright 2023 Camptocamp SA
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
import logging
34

45
from .common import TestMTOVariantCommon
56

@@ -30,11 +31,9 @@ def test_variants_mto(self):
3031
self.toggle_is_mto(black_pen)
3132
self.assertVariantsNotMTO(black_pen)
3233
self.assertVariantsMTO(blue_pen | green_pen | red_pen)
33-
# Disable mto route on the template, variants is_mto is kept
34+
# Disable mto route on the template, reset is_mto on variants
3435
self.remove_route(pen_template, self.mto_route)
35-
# is_mto is unchanged
36-
self.assertVariantsNotMTO(black_pen)
37-
self.assertVariantsMTO(blue_pen | green_pen | red_pen)
36+
self.assertVariantsNotMTO(pens)
3837

3938
def test_template_routes_updated(self):
4039
# instanciate variables
@@ -58,3 +57,52 @@ def test_template_routes_updated(self):
5857
# Template is still MTO, but variants is_mto shouldn't have changed
5958
self.assertVariantsMTO(green_pen | red_pen)
6059
self.assertVariantsNotMTO(black_pen | blue_pen)
60+
61+
def test_template_warnings(self):
62+
# instanciate variables
63+
pen_template = self.template_pen
64+
pens = self.variants_pen
65+
blue_pen = self.blue_pen
66+
red_pen = self.red_pen
67+
green_pen = self.green_pen
68+
black_pen = self.black_pen
69+
onchange_logger = logging.getLogger("odoo.tests.common.onchange")
70+
self.assertVariantsNotMTO(pens)
71+
# enable mto route for black pen
72+
self.toggle_is_mto(black_pen)
73+
self.assertVariantsMTO(black_pen)
74+
# Enable mto route on the template, raise warning as is_mto is reset on variants
75+
with self.assertLogs(onchange_logger) as log:
76+
self.add_route(pen_template, self.mto_route)
77+
self.assertIn("WARNING", log.output[0])
78+
self.assertIn("Activating MTO route will reset", log.output[0])
79+
self.assertVariantsMTO(pens)
80+
# Disable mto route for black pen
81+
self.toggle_is_mto(black_pen)
82+
self.assertVariantsNotMTO(black_pen)
83+
self.assertVariantsMTO(blue_pen | green_pen | red_pen)
84+
# Enable unrelated route does not raise warning nor reset
85+
random_route = self.mto_route.create({"name": "loutourout de la vit"})
86+
with self.assertLogs(onchange_logger) as log:
87+
self.add_route(pen_template, random_route)
88+
onchange_logger.info("No warning raised")
89+
self.assertNotIn("WARNING", log.output[0])
90+
self.assertVariantsNotMTO(black_pen)
91+
self.assertVariantsMTO(blue_pen | green_pen | red_pen)
92+
# Disable mto route on the template, raise warning as is_mto is reset on variants
93+
with self.assertLogs(onchange_logger) as log:
94+
self.remove_route(pen_template, self.mto_route)
95+
self.assertIn("WARNING", log.output[0])
96+
self.assertIn("Deactivating MTO route will reset", log.output[0])
97+
self.assertVariantsNotMTO(pens)
98+
# Enable mto route for black pen
99+
self.toggle_is_mto(black_pen)
100+
self.assertVariantsMTO(black_pen)
101+
self.assertVariantsNotMTO(blue_pen | green_pen | red_pen)
102+
# Disable unrelated route does not raise warning nor reset
103+
with self.assertLogs(onchange_logger) as log:
104+
self.remove_route(pen_template, random_route)
105+
onchange_logger.info("No warning raised")
106+
self.assertNotIn("WARNING", log.output[0])
107+
self.assertVariantsMTO(black_pen)
108+
self.assertVariantsNotMTO(blue_pen | green_pen | red_pen)

0 commit comments

Comments
 (0)