Skip to content

Commit

Permalink
Add mto_route_product_variant
Browse files Browse the repository at this point in the history
  • Loading branch information
mmequignon committed Dec 22, 2023
1 parent 8c24218 commit 2c44ec4
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 0 deletions.
1 change: 1 addition & 0 deletions mto_route_product_variant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions mto_route_product_variant/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

{
"name": "MTO Route Product Variant",
"summary": "Allow to individually set variants as MTO",
"version": "14.0.1.0.0",
"category": "Inventory",
"website": "https://github.com/OCA/stock-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"],
}
2 changes: 2 additions & 0 deletions mto_route_product_variant/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import product_product
from . import product_template
63 changes: 63 additions & 0 deletions mto_route_product_variant/models/product_product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo import api, models, fields

IS_MTO_HELP = """
Check or Uncheck this field if you want this variant to have another
mto configration than its template.
"""

class ProductProduct(models.Model):
_inherit = "product.product"

is_mto = fields.Boolean(
string="Variant is Mto",
compute="_compute_is_mto",
inverse="_inverse_is_mto",
store="true",
help=IS_MTO_HELP,
index=True
)

@api.depends("product_tmpl_id.route_ids")
def _compute_is_mto(self):
# We only want to force all variants `is_mto` to True when
# the mto route is explicitely set on its template.
# Watching the mto route is not enough, since we might
# have a template with the mto route, and disabled the mto route
# for a few of its variants
# If a user sets another route on the variant, we do not want the
# mto disabled variants to be updated.
# To ensure that, the created `has_mto_route_changed` boolean field
# is set to True only when the MTO route is set on a template.
mto_route = self.env.ref("stock.route_warehouse0_mto")
templates = self.product_tmpl_id
# Only variants with a template with the MTO route and has_mto_route_changed
# should be updated.
mto_templates = templates.filtered(
lambda t: mto_route in t.route_ids and t.has_mto_route_changed
)
mto_variants = self.filtered(lambda p: p.product_tmpl_id in mto_templates)
mto_variants.is_mto = True
# For the other variants, keep their current value.
other_variants = self - mto_variants
for variant in other_variants:
variant.is_mto = variant.is_mto
# Then set template's has_mto_route_changed to False, as it
# has been handled above
templates.has_mto_route_changed = False

def _inverse_is_mto(self):
# When all variants of a template are `is_mto == False`, drop the MTO route
# from the template, otherwise do nothing
mto_route = self.env.ref("stock.route_warehouse0_mto")
for template in self.product_tmpl_id:
is_mto = False
for variant in template.product_variant_ids:
if variant.is_mto:
is_mto = True
break
# If no variant is mto, then drop the route of the template.
if not is_mto:
template.route_ids = [(3, mto_route.id, 0)]
24 changes: 24 additions & 0 deletions mto_route_product_variant/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo import models, fields

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

has_mto_route_changed = fields.Boolean()

def write(self, values):
if not "route_ids" in values:
return super().write(values)
# This route_ids change will trigger variant's _compute_is_mto.
# We want to set variant's is_mto to True only if their
# template has been set to True here ↓
mto_route = self.env.ref("stock.route_warehouse0_mto")
template_not_mto_before = self.filtered(lambda t: mto_route not in t.route_ids)
res = super().write(values)
template_mto_after = self.filtered(lambda t: mto_route in t.route_ids)
# Templates where mto route has changed are those where
# the mto route has been set
templates_mto_set = template_not_mto_before & template_mto_after
templates_mto_set.has_mto_route_changed = True
1 change: 1 addition & 0 deletions mto_route_product_variant/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Matthieu Méquignon <[email protected]>
1 change: 1 addition & 0 deletions mto_route_product_variant/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow to individually set variants as MTO
4 changes: 4 additions & 0 deletions mto_route_product_variant/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This module is useless on its own.

However, it is meant to give the ability to check if a product variant is MTO,
rather than checking its template's routes.
1 change: 1 addition & 0 deletions mto_route_product_variant/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_mto_variant
76 changes: 76 additions & 0 deletions mto_route_product_variant/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo.tests.common import Form, SavepointCase


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

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
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_cache(["is_mto"])
self.assertTrue(all([record.is_mto for record in records]))

def assertVariantsNotMTO(self, records):
records.invalidate_cache(["is_mto"])
self.assertFalse(any([record.is_mto for record in records]))
60 changes: 60 additions & 0 deletions mto_route_product_variant/tests/test_mto_variant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from .common import TestMTOVariantCommon


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
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, variants is_mto is kept
self.remove_route(pen_template, self.mto_route)
# is_mto is unchanged
self.assertVariantsNotMTO(black_pen)
self.assertVariantsMTO(blue_pen | green_pen | red_pen)

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
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)
30 changes: 30 additions & 0 deletions mto_route_product_variant/views/product_product.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2023 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>

<record id="product_normal_form_view" model="ir.ui.view">
<field name="name">product.product.form.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<group name="operations" position="inside">
<field name="is_mto"/>
</group>
</field>
</record>

<record id="product_variant_easy_edit_view" model="ir.ui.view">
<field name="name">product.product.form.easy.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
<field name="arch" type="xml">
<group name="weight" position="inside">
<field name="is_mto"/>
</group>
</field>
</record>



</odoo>
6 changes: 6 additions & 0 deletions setup/mto_route_product_variant/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

0 comments on commit 2c44ec4

Please sign in to comment.