-
-
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 all 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,16 @@ | ||
# 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,39 @@ | ||||||
# 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 models | ||||||
from odoo.osv.expression import AND | ||||||
|
||||||
|
||||||
class ProductProduct(models.Model): | ||||||
_inherit = "product.product" | ||||||
|
||||||
def _variant_is_mto(self): | ||||||
self.ensure_one() | ||||||
return self.is_mto | ||||||
|
||||||
def write(self, vals): | ||||||
res = super().write(vals) | ||||||
if "is_mto" in vals and vals["is_mto"] == False: | ||||||
self._archive_orderpoints_on_mto_removal() | ||||||
return res | ||||||
|
||||||
def _get_orderpoints_to_archive_domain(self, warehouse): | ||||||
# 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? |
||||||
domain = warehouse._get_orderpoints_to_archive_domain() | ||||||
if self: | ||||||
domain = AND( | ||||||
[ | ||||||
domain, | ||||||
[("product_id", "in", self.ids)], | ||||||
] | ||||||
) | ||||||
return domain | ||||||
|
||||||
def _archive_orderpoints_on_mto_removal(self): | ||||||
warehouses = self.env["stock.warehouse"].search([]) | ||||||
for wh in warehouses: | ||||||
domain = self._get_orderpoints_to_archive_domain(wh) | ||||||
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,155 @@ | ||||||
# 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") | ||||||
cls.warehouse.mto_as_mts = True | ||||||
|
||||||
@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_warehouse_archive(self): | ||||||
black_pen = self.black_pen | ||||||
order = self._create_sale_order(black_pen) | ||||||
order.action_confirm() | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertTrue(orderpoint) | ||||||
self.warehouse.mto_as_mts = False | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertFalse(orderpoint) | ||||||
|
||||||
def test_mto_as_mts_orderpoint_product_archive(self): | ||||||
black_pen = self.black_pen | ||||||
order = self._create_sale_order(black_pen) | ||||||
order.action_confirm() | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertTrue(orderpoint) | ||||||
self.toggle_is_mto(black_pen) | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertFalse(orderpoint) | ||||||
|
||||||
def test_mto_as_mts_orderpoint(self): | ||||||
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) | ||||||
self.assertIn(self.mto_route, black_pen.route_ids) | ||||||
self.assertTrue(black_pen.is_mto) | ||||||
order.action_confirm() | ||||||
orderpoint = self._get_orderpoint_for_products(black_pen) | ||||||
self.assertTrue(orderpoint) | ||||||
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): | ||||||
self.assertIn(self.buy_route, self.template_pen.route_ids) | ||||||
template_pen = self.template_pen | ||||||
black_pen = self.black_pen | ||||||
variants_pen = self.variants_pen | ||||||
self.assertIn(self.buy_route, black_pen.route_ids) | ||||||
# set everything to not mto | ||||||
template_pen.write({"route_ids": [(3, self.mto_route.id, 0)]}) | ||||||
self.assertIn(self.buy_route, black_pen.route_ids) | ||||||
self.assertNotIn(self.mto_route, black_pen.route_ids) | ||||||
self.toggle_is_mto(variants_pen) | ||||||
self.assertIn(self.mto_route, black_pen.route_ids) | ||||||
self.assertIn(self.buy_route, black_pen.route_ids) | ||||||
self.toggle_is_mto(variants_pen) | ||||||
self.assertNotIn(self.mto_route, black_pen.route_ids) | ||||||
self.assertIn(self.buy_route, black_pen.route_ids) | ||||||
# 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-logistics-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 |
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