Skip to content

Commit bc17928

Browse files
committed
Add sale_stock_mto_as_mts_orderpoint_product_variant
1 parent 7775e2a commit bc17928

File tree

10 files changed

+201
-0
lines changed

10 files changed

+201
-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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2024 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
{
5+
"name": "Sale Stock MTO as MTS Orderpoint Product Variant",
6+
"version": "14.0.1.0.0",
7+
"development_status": "Alpha",
8+
"category": "Operations/Inventory/Delivery",
9+
"website": "https://github.com/OCA/stock-logistics-workflow",
10+
"author": "Camptocamp, Odoo Community Association (OCA)",
11+
"maintainers": ["mmequignon"],
12+
"license": "AGPL-3",
13+
"installable": True,
14+
"auto_install": True,
15+
"depends": [
16+
"sale_stock_mto_as_mts_orderpoint",
17+
"stock_product_variant_mto",
18+
],
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import product_product
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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
5+
6+
class ProductProduct(models.Model):
7+
_inherit = "product.product"
8+
9+
def _variant_is_mto(self):
10+
self.ensure_one()
11+
return self.is_mto
12+
13+
def _inverse_is_mto(self):
14+
res = super()._inverse_is_mto()
15+
self._archive_orderpoints_on_mto_removal()
16+
return res
17+
18+
@api.depends("product_tmpl_id.route_ids")
19+
def _compute_is_mto(self):
20+
# Archive orderpoints when variant becomes not mto
21+
res = super()._compute_is_mto()
22+
self._archive_orderpoints_on_mto_removal()
23+
return res
24+
25+
def _get_orderpoints_to_archive_domain(self):
26+
# Orderpoints to archive are those where
27+
warehouses = self.env["stock.warehouse"].search([])
28+
locations = warehouses._get_locations_for_mto_orderpoints()
29+
return [
30+
("product_id", "in", self.ids),
31+
("product_min_qty", "=", 0.0),
32+
("product_max_qty", "=", 0.0),
33+
("location_id", "in", locations.ids),
34+
("product_id.is_mto", "=", False),
35+
]
36+
37+
def _archive_orderpoints_on_mto_removal(self):
38+
domain = self._get_orderpoints_to_archive_domain()
39+
ops = self.env["stock.warehouse.orderpoint"].search(domain)
40+
if ops:
41+
ops.write({"active": False})
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This module extends the `sale_stock_mto_as_mts_orderpoint` module,
2+
in order to handle the orderpoints at variant level.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_mto_as_mts_variant
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright 2023 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from odoo.tests import Form
5+
from odoo.addons.stock_product_variant_mto.tests.common import TestMTOVariantCommon
6+
7+
8+
class TestMtoAsMtsVariant(TestMTOVariantCommon):
9+
10+
@classmethod
11+
def setUpClass(cls):
12+
super().setUpClass()
13+
cls.partner = cls.env.ref("base.res_partner_2")
14+
cls.vendor_partner = cls.env.ref("base.res_partner_12")
15+
cls.env["product.supplierinfo"].create(
16+
[
17+
{
18+
"name": cls.vendor_partner.id,
19+
"product_tmpl_id": variant.product_tmpl_id.id,
20+
"product_id": variant.id,
21+
"min_qty": 1.0,
22+
"price": 1.0,
23+
}
24+
for variant in cls.variants_pen
25+
]
26+
)
27+
cls.warehouse = cls.env.ref("stock.warehouse0")
28+
cls.warehouse.mto_as_mts = True
29+
30+
@classmethod
31+
def setUpClassProduct(cls):
32+
super().setUpClassProduct()
33+
cls.buy_route = cls.env.ref("purchase_stock.route_warehouse0_buy")
34+
cls.template_pen.write(
35+
{"route_ids": [(6, 0, [cls.buy_route.id, cls.mto_route.id])]}
36+
)
37+
38+
@classmethod
39+
def _create_sale_order(cls, products):
40+
sale_form = Form(cls.env["sale.order"])
41+
sale_form.partner_id = cls.partner
42+
sale_form.warehouse_id = cls.warehouse
43+
for product in products:
44+
with sale_form.order_line.new() as line_form:
45+
line_form.product_id = product
46+
line_form.product_uom_qty = 1
47+
return sale_form.save()
48+
49+
def _get_orderpoint_for_products(self, products, archived=False):
50+
orderpoint = self.env["stock.warehouse.orderpoint"]
51+
if archived:
52+
orderpoint = orderpoint.with_context(active_test=False)
53+
return orderpoint.search(
54+
[("product_id", "in", products.ids)]
55+
)
56+
57+
def test_mto_as_mts_orderpoint(self):
58+
template_pen = self.template_pen
59+
black_pen = self.black_pen
60+
blue_pen = self.blue_pen
61+
red_pen = self.red_pen
62+
green_pen = self.green_pen
63+
order = self._create_sale_order(black_pen)
64+
orderpoint = self._get_orderpoint_for_products(black_pen)
65+
self.assertFalse(orderpoint)
66+
order.action_confirm()
67+
orderpoint = self._get_orderpoint_for_products(black_pen)
68+
self.assertEqual(
69+
orderpoint.location_id,
70+
self.warehouse._get_locations_for_mto_orderpoints(),
71+
)
72+
self.assertAlmostEqual(orderpoint.product_min_qty, 0.0)
73+
self.assertAlmostEqual(orderpoint.product_max_qty, 0.0)
74+
# Setting the black pen to mto should drop its orderpoint
75+
self.toggle_is_mto(black_pen)
76+
orderpoint = self._get_orderpoint_for_products(black_pen)
77+
self.assertFalse(orderpoint)
78+
# Creating and confirming an order for variants should create
79+
# an orderpoint for all variants but the black pen
80+
order = self._create_sale_order(self.variants_pen)
81+
order.action_confirm()
82+
# black pen orderpoint is archived
83+
self.assertFalse(self._get_orderpoint_for_products(black_pen))
84+
self.assertTrue(self._get_orderpoint_for_products(black_pen, archived=True))
85+
other_pens = red_pen | green_pen | blue_pen
86+
self.assertEqual(
87+
len(self._get_orderpoint_for_products(other_pens)), 3
88+
)
89+
90+
def test_mtp_as_mts_orderpoint_product_no_mto(self):
91+
template_pen = self.template_pen
92+
black_pen = self.black_pen
93+
variants_pen = self.variants_pen
94+
# set everything to not mto
95+
template_pen.route_ids = False
96+
self.toggle_is_mto(variants_pen)
97+
# then check that no orderpoint is created
98+
order = self._create_sale_order(black_pen)
99+
orderpoint = self.env["stock.warehouse.orderpoint"].search(
100+
[("product_id", "=", black_pen.id)]
101+
)
102+
self.assertFalse(orderpoint)
103+
order.action_confirm()
104+
orderpoint = self.env["stock.warehouse.orderpoint"].search(
105+
[("product_id", "=", black_pen.id)]
106+
)
107+
self.assertFalse(orderpoint)
108+
109+
def test_cancel_sale_order_orderpoint(self):
110+
order = self._create_sale_order(self.variants_pen)
111+
order.action_confirm()
112+
order.action_cancel()
113+
order.action_draft()
114+
order.action_confirm()
115+
self.assertEqual(order.state, "sale")
116+
117+
def test_confirm_mto_as_mts_sudo_needed(self):
118+
"""Check access right needed to confirm sale.
119+
120+
A sale manager user with no right on inventory will raise an access
121+
right error on confirmation.
122+
This is the why of the sudo in `sale_stock_mto_as_mts_orderpoint`
123+
"""
124+
user = self.env.ref("base.user_demo")
125+
sale_group = self.env.ref("sales_team.group_sale_manager")
126+
sale_group.users = [(4, user.id)]
127+
order = self._create_sale_order(self.variants_pen)
128+
order.with_user(user).action_confirm()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../sale_stock_mto_as_mts_orderpoint_product_variant
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import setuptools
2+
3+
setuptools.setup(
4+
setup_requires=['setuptools-odoo'],
5+
odoo_addon=True,
6+
)

0 commit comments

Comments
 (0)