-
-
Notifications
You must be signed in to change notification settings - Fork 671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[14.0][IMP/ADD] stock_product_variant_mto, sale_stock_mto_as_mts_orderpoint_product_variant: Handle mto route at variant level #1461
base: 14.0
Are you sure you want to change the base?
Changes from 5 commits
d75564e
deef52a
8b6a7de
d2dd13e
d634638
3d81b69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
from . import product | ||
from . import product_template | ||
from . import product_product | ||
from . import sale_order | ||
from . import stock_move | ||
from . import stock_warehouse |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Copyright 2023 Camptocamp SA | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) | ||
|
||
from odoo import models | ||
|
||
class ProductProduct(models.Model): | ||
_inherit = "product.product" | ||
|
||
def _variant_is_mto(self): | ||
self.ensure_one() | ||
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) | ||
return mto_route in self.route_ids | ||
|
||
def _variant_is_not_mto(self): | ||
return not self._variant_is_mto() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Copyright 2024 Camptocamp SA | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) | ||
|
||
{ | ||
mmequignon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"name": "Sale Stock MTO as MTS Orderpoint Product Variant", | ||
"version": "14.0.1.0.0", | ||
"development_status": "Alpha", | ||
"category": "Operations/Inventory/Delivery", | ||
"website": "https://github.com/OCA/stock-logistics-workflow", | ||
"author": "Camptocamp, Odoo Community Association (OCA)", | ||
"maintainers": ["mmequignon"], | ||
"license": "AGPL-3", | ||
"installable": True, | ||
"auto_install": True, | ||
"depends": [ | ||
"sale_stock_mto_as_mts_orderpoint", | ||
"stock_product_variant_mto", | ||
], | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import product_product |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,41 @@ | ||||||
# Copyright 2023 Camptocamp SA | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) | ||||||
|
||||||
from odoo import api, models | ||||||
|
||||||
class ProductProduct(models.Model): | ||||||
_inherit = "product.product" | ||||||
|
||||||
def _variant_is_mto(self): | ||||||
self.ensure_one() | ||||||
return self.is_mto | ||||||
|
||||||
def _inverse_is_mto(self): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is this defined on the field? I don't think this is called. |
||||||
res = super()._inverse_is_mto() | ||||||
self._archive_orderpoints_on_mto_removal() | ||||||
return res | ||||||
|
||||||
@api.depends("product_tmpl_id.route_ids") | ||||||
def _compute_is_mto(self): | ||||||
# Archive orderpoints when variant becomes not mto | ||||||
res = super()._compute_is_mto() | ||||||
self._archive_orderpoints_on_mto_removal() | ||||||
return res | ||||||
|
||||||
def _get_orderpoints_to_archive_domain(self): | ||||||
# Orderpoints to archive are those where | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete comment? |
||||||
warehouses = self.env["stock.warehouse"].search([]) | ||||||
locations = warehouses._get_locations_for_mto_orderpoints() | ||||||
return [ | ||||||
("product_id", "in", self.ids), | ||||||
("product_min_qty", "=", 0.0), | ||||||
("product_max_qty", "=", 0.0), | ||||||
("location_id", "in", locations.ids), | ||||||
("product_id.is_mto", "=", False), | ||||||
] | ||||||
|
||||||
def _archive_orderpoints_on_mto_removal(self): | ||||||
domain = self._get_orderpoints_to_archive_domain() | ||||||
ops = self.env["stock.warehouse.orderpoint"].search(domain) | ||||||
if ops: | ||||||
ops.write({"active": False}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Matthieu Méquignon <[email protected]> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
This module extends the `sale_stock_mto_as_mts_orderpoint` module, | ||
in order to handle the orderpoints at variant level. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_mto_as_mts_variant |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,127 @@ | ||||||
# Copyright 2023 Camptocamp SA | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) | ||||||
|
||||||
from odoo.tests import Form | ||||||
from odoo.addons.stock_product_variant_mto.tests.common import TestMTOVariantCommon | ||||||
|
||||||
|
||||||
class TestMtoAsMtsVariant(TestMTOVariantCommon): | ||||||
|
||||||
@classmethod | ||||||
def setUpClass(cls): | ||||||
super().setUpClass() | ||||||
cls.partner = cls.env.ref("base.res_partner_2") | ||||||
cls.vendor_partner = cls.env.ref("base.res_partner_12") | ||||||
cls.env["product.supplierinfo"].create( | ||||||
[ | ||||||
{ | ||||||
"name": cls.vendor_partner.id, | ||||||
"product_tmpl_id": variant.product_tmpl_id.id, | ||||||
"product_id": variant.id, | ||||||
"min_qty": 1.0, | ||||||
"price": 1.0, | ||||||
} | ||||||
for variant in cls.variants_pen | ||||||
] | ||||||
) | ||||||
cls.warehouse = cls.env.ref("stock.warehouse0") | ||||||
|
||||||
@classmethod | ||||||
def setUpClassProduct(cls): | ||||||
super().setUpClassProduct() | ||||||
cls.buy_route = cls.env.ref("purchase_stock.route_warehouse0_buy") | ||||||
cls.template_pen.write( | ||||||
{"route_ids": [(6, 0, [cls.buy_route.id, cls.mto_route.id])]} | ||||||
) | ||||||
|
||||||
@classmethod | ||||||
def _create_sale_order(cls, products): | ||||||
sale_form = Form(cls.env["sale.order"]) | ||||||
sale_form.partner_id = cls.partner | ||||||
sale_form.warehouse_id = cls.warehouse | ||||||
for product in products: | ||||||
with sale_form.order_line.new() as line_form: | ||||||
line_form.product_id = product | ||||||
line_form.product_uom_qty = 1 | ||||||
return sale_form.save() | ||||||
|
||||||
def _get_orderpoint_for_products(self, products, archived=False): | ||||||
orderpoint = self.env["stock.warehouse.orderpoint"] | ||||||
if archived: | ||||||
orderpoint = orderpoint.with_context(active_test=False) | ||||||
return orderpoint.search( | ||||||
[("product_id", "in", products.ids)] | ||||||
) | ||||||
|
||||||
def test_mto_as_mts_orderpoint(self): | ||||||
template_pen = self.template_pen | ||||||
black_pen = self.black_pen | ||||||
blue_pen = self.blue_pen | ||||||
red_pen = self.red_pen | ||||||
green_pen = self.green_pen | ||||||
order = self._create_sale_order(black_pen) | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertFalse(orderpoint) | ||||||
order.action_confirm() | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertEqual( | ||||||
orderpoint.location_id, | ||||||
self.warehouse._get_locations_for_mto_orderpoints(), | ||||||
) | ||||||
self.assertAlmostEqual(orderpoint.product_min_qty, 0.0) | ||||||
self.assertAlmostEqual(orderpoint.product_max_qty, 0.0) | ||||||
# Setting the black pen to mto should drop its orderpoint | ||||||
self.toggle_is_mto(black_pen) | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertFalse(orderpoint) | ||||||
# Creating and confirming an order for variants should create | ||||||
# an orderpoint for all variants but the black pen | ||||||
order = self._create_sale_order(self.variants_pen) | ||||||
order.action_confirm() | ||||||
# black pen orderpoint is archived | ||||||
self.assertFalse(self._get_orderpoint_for_products(black_pen)) | ||||||
self.assertTrue(self._get_orderpoint_for_products(black_pen, archived=True)) | ||||||
other_pens = red_pen | green_pen | blue_pen | ||||||
self.assertEqual( | ||||||
len(self._get_orderpoint_for_products(other_pens)), 3 | ||||||
) | ||||||
|
||||||
def test_mtp_as_mts_orderpoint_product_no_mto(self): | ||||||
template_pen = self.template_pen | ||||||
black_pen = self.black_pen | ||||||
variants_pen = self.variants_pen | ||||||
# set everything to not mto | ||||||
template_pen.route_ids = False | ||||||
self.toggle_is_mto(variants_pen) | ||||||
# then check that no orderpoint is created | ||||||
order = self._create_sale_order(black_pen) | ||||||
orderpoint = self.env["stock.warehouse.orderpoint"].search( | ||||||
[("product_id", "=", black_pen.id)] | ||||||
) | ||||||
self.assertFalse(orderpoint) | ||||||
order.action_confirm() | ||||||
orderpoint = self.env["stock.warehouse.orderpoint"].search( | ||||||
[("product_id", "=", black_pen.id)] | ||||||
) | ||||||
self.assertFalse(orderpoint) | ||||||
|
||||||
def test_cancel_sale_order_orderpoint(self): | ||||||
order = self._create_sale_order(self.variants_pen) | ||||||
order.action_confirm() | ||||||
order.action_cancel() | ||||||
order.action_draft() | ||||||
order.action_confirm() | ||||||
self.assertEqual(order.state, "sale") | ||||||
|
||||||
def test_confirm_mto_as_mts_sudo_needed(self): | ||||||
"""Check access right needed to confirm sale. | ||||||
|
||||||
A sale manager user with no right on inventory will raise an access | ||||||
right error on confirmation. | ||||||
This is the why of the sudo in `sale_stock_mto_as_mts_orderpoint` | ||||||
""" | ||||||
user = self.env.ref("base.user_demo") | ||||||
sale_group = self.env.ref("sales_team.group_sale_manager") | ||||||
sale_group.users = [(4, user.id)] | ||||||
order = self._create_sale_order(self.variants_pen) | ||||||
order.with_user(user).action_confirm() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../sale_stock_mto_as_mts_orderpoint_product_variant |
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, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../stock_product_variant_mto |
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, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,18 @@ | ||||||
# Copyright 2023 Camptocamp SA | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# 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": "14.0.1.0.0", | ||||||
"development_status": "Alpha", | ||||||
"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"], | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import product_product | ||
from . import product_template |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,51 @@ | ||||||
# Copyright 2023 Camptocamp SA | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# 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 to enable the Make To Order on the variant, | ||||||
independantly from its template configuration.\n | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If find having this field on |
||||||
"stock.location.route", | ||||||
compute="_compute_route_ids", | ||||||
domain="[('product_selectable', '=', True)]", | ||||||
store=False | ||||||
) | ||||||
|
||||||
def _compute_is_mto(self): | ||||||
mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) | ||||||
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 |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,43 @@ | ||||||
# Copyright 2023 Camptocamp SA | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) | ||||||
|
||||||
from odoo import _, api, models, fields | ||||||
|
||||||
class ProductTemplate(models.Model): | ||||||
_inherit = "product.template" | ||||||
|
||||||
def write(self, values): | ||||||
if not "route_ids" in values: | ||||||
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) | ||||||
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_mto_removed).product_variant_ids._compute_is_mto() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
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: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we can make the condition more selective and raise the warning only if we have multiple variants, and these variants have different MTO settings. |
||||||
# Return warning activating MTO route | ||||||
return { | ||||||
"warning": { | ||||||
"title": _("Warning"), | ||||||
"message": _("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: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above |
||||||
# Return warning deactivating MTO route | ||||||
return { | ||||||
"warning": { | ||||||
"title": _("Warning"), | ||||||
"message": _("Deactivating MTO route will reset `Variant is MTO` setting on the variants.") | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent years