Skip to content

Commit 8265865

Browse files
committed
Add mto_route_product_variant
1 parent 8c24218 commit 8265865

File tree

14 files changed

+288
-0
lines changed

14 files changed

+288
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2023 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
{
5+
"name": "MTO Route Product Variant",
6+
"summary": "Allow to individually set variants as MTO",
7+
"version": "14.0.1.0.0",
8+
"development_status": "Alpha",
9+
"category": "Inventory",
10+
"website": "https://github.com/OCA/stock-workflow",
11+
"author": "Camptocamp SA, Odoo Community Association (OCA)",
12+
"maintainers": ["mmequignon"],
13+
"license": "AGPL-3",
14+
"installable": True,
15+
"auto_install": False,
16+
"depends": ["stock"],
17+
"data": ["views/product_product.xml"],
18+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product_product
2+
from . import product_template
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2023 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from odoo import api, models, fields
5+
6+
IS_MTO_HELP = """
7+
Check or Uncheck this field if you want this variant to have another
8+
mto configration than its template.
9+
"""
10+
11+
class ProductProduct(models.Model):
12+
_inherit = "product.product"
13+
14+
is_mto = fields.Boolean(
15+
string="Variant is Mto",
16+
compute="_compute_is_mto",
17+
inverse="_inverse_is_mto",
18+
store="true",
19+
help=IS_MTO_HELP,
20+
index=True
21+
)
22+
23+
@api.depends("product_tmpl_id.route_ids")
24+
def _compute_is_mto(self):
25+
# We only want to force all variants `is_mto` to True when
26+
# the mto route is explicitely set on its template.
27+
# Watching the mto route is not enough, since we might
28+
# have a template with the mto route, and disabled the mto route
29+
# for a few of its variants
30+
# If a user sets another route on the variant, we do not want the
31+
# mto disabled variants to be updated.
32+
# To ensure that, the created `has_mto_route_changed` boolean field
33+
# is set to True only when the MTO route is set on a template.
34+
mto_route = self.env.ref("stock.route_warehouse0_mto")
35+
templates = self.product_tmpl_id
36+
# Only variants with a template with the MTO route and has_mto_route_changed
37+
# should be updated.
38+
mto_templates = templates.filtered(
39+
lambda t: mto_route in t.route_ids and t.has_mto_route_changed
40+
)
41+
mto_variants = self.filtered(lambda p: p.product_tmpl_id in mto_templates)
42+
mto_variants.is_mto = True
43+
# For the other variants, keep their current value.
44+
other_variants = self - mto_variants
45+
for variant in other_variants:
46+
variant.is_mto = variant.is_mto
47+
# Then set template's has_mto_route_changed to False, as it
48+
# has been handled above
49+
templates.has_mto_route_changed = False
50+
51+
def _inverse_is_mto(self):
52+
# When all variants of a template are `is_mto == False`, drop the MTO route
53+
# from the template, otherwise do nothing
54+
mto_route = self.env.ref("stock.route_warehouse0_mto")
55+
for template in self.product_tmpl_id:
56+
is_mto = False
57+
for variant in template.product_variant_ids:
58+
if variant.is_mto:
59+
is_mto = True
60+
break
61+
# If no variant is mto, then drop the route of the template.
62+
if not is_mto:
63+
template.route_ids = [(3, mto_route.id, 0)]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright 2023 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from odoo import models, fields
5+
6+
class ProductTemplate(models.Model):
7+
_inherit = "product.template"
8+
9+
has_mto_route_changed = fields.Boolean()
10+
11+
def write(self, values):
12+
if not "route_ids" in values:
13+
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 ↓
17+
mto_route = self.env.ref("stock.route_warehouse0_mto")
18+
template_not_mto_before = self.filtered(lambda t: mto_route not in t.route_ids)
19+
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
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Matthieu Méquignon <[email protected]>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow to individually set variants as MTO
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
This module is useless on its own.
2+
3+
However, it is meant to give the ability to check if a product variant is MTO,
4+
rather than checking its template's routes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_mto_variant
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright 2023 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from odoo.tests.common import Form, SavepointCase
5+
6+
7+
class TestMTOVariantCommon(SavepointCase):
8+
at_install = False
9+
post_install = True
10+
11+
@classmethod
12+
def setUpClass(cls):
13+
super().setUpClass()
14+
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
15+
cls.setUpClassProduct()
16+
17+
18+
@classmethod
19+
def setUpClassProduct(cls):
20+
cls.color = cls.env['product.attribute'].create({'name': 'Color'})
21+
value_model = cls.env['product.attribute.value']
22+
cls.values = value_model.create(
23+
[
24+
{'name': 'red', 'attribute_id': cls.color.id},
25+
{'name': 'blue', 'attribute_id': cls.color.id},
26+
{'name': 'black', 'attribute_id': cls.color.id},
27+
{'name': 'green', 'attribute_id': cls.color.id},
28+
]
29+
)
30+
cls.value_red = cls.values.filtered(lambda v: v.name == "red")
31+
cls.value_blue = cls.values.filtered(lambda v: v.name == "blue")
32+
cls.value_black = cls.values.filtered(lambda v: v.name == "black")
33+
cls.value_green = cls.values.filtered(lambda v: v.name == "green")
34+
cls.template_pen = cls.env["product.template"].create(
35+
{
36+
"name": "pen",
37+
'attribute_line_ids': [
38+
(0, 0, {
39+
'attribute_id': cls.color.id,
40+
'value_ids': [(6, 0, cls.values.ids)],
41+
})
42+
]
43+
}
44+
)
45+
cls.variants_pen = cls.template_pen.product_variant_ids
46+
cls.black_pen = cls.variants_pen.filtered(lambda v: v.product_template_attribute_value_ids.name == "black")
47+
cls.green_pen = cls.variants_pen.filtered(lambda v: v.product_template_attribute_value_ids.name == "green")
48+
cls.red_pen = cls.variants_pen.filtered(lambda v: v.product_template_attribute_value_ids.name == "red")
49+
cls.blue_pen = cls.variants_pen.filtered(lambda v: v.product_template_attribute_value_ids.name == "blue")
50+
cls.mto_route = cls.env.ref("stock.route_warehouse0_mto")
51+
cls.mto_route.active = True
52+
53+
def add_route(self, template, route):
54+
if not route:
55+
route = self.mto_route
56+
with Form(template) as record:
57+
record.route_ids.add(route)
58+
59+
def remove_route(self, template, route):
60+
if not route:
61+
route = self.mto_route
62+
with Form(template) as record:
63+
record.route_ids.remove(id=route.id)
64+
65+
@classmethod
66+
def toggle_is_mto(self, records):
67+
for record in records:
68+
record.is_mto = not record.is_mto
69+
70+
def assertVariantsMTO(self, records):
71+
records.invalidate_cache(["is_mto"])
72+
self.assertTrue(all([record.is_mto for record in records]))
73+
74+
def assertVariantsNotMTO(self, records):
75+
records.invalidate_cache(["is_mto"])
76+
self.assertFalse(any([record.is_mto for record in records]))

0 commit comments

Comments
 (0)