Skip to content

Commit e694809

Browse files
committed
Add sale_stock_mto_as_mts_orderpoint_product_variant
1 parent 4046940 commit e694809

File tree

10 files changed

+200
-0
lines changed

10 files changed

+200
-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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
29+
@classmethod
30+
def setUpClassProduct(cls):
31+
super().setUpClassProduct()
32+
cls.buy_route = cls.env.ref("purchase_stock.route_warehouse0_buy")
33+
cls.template_pen.write(
34+
{"route_ids": [(6, 0, [cls.buy_route.id, cls.mto_route.id])]}
35+
)
36+
37+
@classmethod
38+
def _create_sale_order(cls, products):
39+
sale_form = Form(cls.env["sale.order"])
40+
sale_form.partner_id = cls.partner
41+
sale_form.warehouse_id = cls.warehouse
42+
for product in products:
43+
with sale_form.order_line.new() as line_form:
44+
line_form.product_id = product
45+
line_form.product_uom_qty = 1
46+
return sale_form.save()
47+
48+
def _get_orderpoint_for_products(self, products, archived=False):
49+
orderpoint = self.env["stock.warehouse.orderpoint"]
50+
if archived:
51+
orderpoint = orderpoint.with_context(active_test=False)
52+
return orderpoint.search(
53+
[("product_id", "in", products.ids)]
54+
)
55+
56+
def test_mto_as_mts_orderpoint(self):
57+
template_pen = self.template_pen
58+
black_pen = self.black_pen
59+
blue_pen = self.blue_pen
60+
red_pen = self.red_pen
61+
green_pen = self.green_pen
62+
order = self._create_sale_order(black_pen)
63+
orderpoint = self._get_orderpoint_for_products(black_pen)
64+
self.assertFalse(orderpoint)
65+
order.action_confirm()
66+
orderpoint = self._get_orderpoint_for_products(black_pen)
67+
self.assertEqual(
68+
orderpoint.location_id,
69+
self.warehouse._get_locations_for_mto_orderpoints(),
70+
)
71+
self.assertAlmostEqual(orderpoint.product_min_qty, 0.0)
72+
self.assertAlmostEqual(orderpoint.product_max_qty, 0.0)
73+
# Setting the black pen to mto should drop its orderpoint
74+
self.toggle_is_mto(black_pen)
75+
orderpoint = self._get_orderpoint_for_products(black_pen)
76+
self.assertFalse(orderpoint)
77+
# Creating and confirming an order for variants should create
78+
# an orderpoint for all variants but the black pen
79+
order = self._create_sale_order(self.variants_pen)
80+
order.action_confirm()
81+
# black pen orderpoint is archived
82+
self.assertFalse(self._get_orderpoint_for_products(black_pen))
83+
self.assertTrue(self._get_orderpoint_for_products(black_pen, archived=True))
84+
other_pens = red_pen | green_pen | blue_pen
85+
self.assertEqual(
86+
len(self._get_orderpoint_for_products(other_pens)), 3
87+
)
88+
89+
def test_mtp_as_mts_orderpoint_product_no_mto(self):
90+
template_pen = self.template_pen
91+
black_pen = self.black_pen
92+
variants_pen = self.variants_pen
93+
# set everything to not mto
94+
template_pen.route_ids = False
95+
self.toggle_is_mto(variants_pen)
96+
# then check that no orderpoint is created
97+
order = self._create_sale_order(black_pen)
98+
orderpoint = self.env["stock.warehouse.orderpoint"].search(
99+
[("product_id", "=", black_pen.id)]
100+
)
101+
self.assertFalse(orderpoint)
102+
order.action_confirm()
103+
orderpoint = self.env["stock.warehouse.orderpoint"].search(
104+
[("product_id", "=", black_pen.id)]
105+
)
106+
self.assertFalse(orderpoint)
107+
108+
def test_cancel_sale_order_orderpoint(self):
109+
order = self._create_sale_order(self.variants_pen)
110+
order.action_confirm()
111+
order.action_cancel()
112+
order.action_draft()
113+
order.action_confirm()
114+
self.assertEqual(order.state, "sale")
115+
116+
def test_confirm_mto_as_mts_sudo_needed(self):
117+
"""Check access right needed to confirm sale.
118+
119+
A sale manager user with no right on inventory will raise an access
120+
right error on confirmation.
121+
This is the why of the sudo in `sale_stock_mto_as_mts_orderpoint`
122+
"""
123+
user = self.env.ref("base.user_demo")
124+
sale_group = self.env.ref("sales_team.group_sale_manager")
125+
sale_group.users = [(4, user.id)]
126+
order = self._create_sale_order(self.variants_pen)
127+
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)