diff --git a/sale_stock_cancel_restriction/__manifest__.py b/sale_stock_cancel_restriction/__manifest__.py index 9cbb4cc5300..bd5aeada257 100644 --- a/sale_stock_cancel_restriction/__manifest__.py +++ b/sale_stock_cancel_restriction/__manifest__.py @@ -10,5 +10,6 @@ "license": "AGPL-3", "development_status": "Production/Stable", "depends": ["sale_stock"], + "data": ["views/stock_warehouse.xml"], "installable": True, } diff --git a/sale_stock_cancel_restriction/models/__init__.py b/sale_stock_cancel_restriction/models/__init__.py index cbd69320b05..cc94ac115bc 100644 --- a/sale_stock_cancel_restriction/models/__init__.py +++ b/sale_stock_cancel_restriction/models/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import sale_order +from . import stock_warehouse diff --git a/sale_stock_cancel_restriction/models/sale_order.py b/sale_stock_cancel_restriction/models/sale_order.py index f8499c5c07e..a5c06d554ee 100644 --- a/sale_stock_cancel_restriction/models/sale_order.py +++ b/sale_stock_cancel_restriction/models/sale_order.py @@ -8,9 +8,11 @@ class SaleOrder(models.Model): _inherit = "sale.order" def action_cancel(self): - """Force to call the cancel method on done picking for having the - expected error, as Odoo has now filter out such pickings from the - cancel operation. - """ - self.mapped("picking_ids").filtered(lambda r: r.state == "done").action_cancel() + # Force to call the cancel method on done picking for having the + # expected error, as Odoo has now filter out such pickings from the + # cancel operation. + domain = [("state", "=", "done")] + if self.warehouse_id.restrict_sale_cancel_after_delivery: + domain += [("picking_type_id.code", "=", "outgoing")] + self.picking_ids.filtered_domain(domain).action_cancel() return super().action_cancel() diff --git a/sale_stock_cancel_restriction/models/stock_warehouse.py b/sale_stock_cancel_restriction/models/stock_warehouse.py new file mode 100644 index 00000000000..fcc4d3d9916 --- /dev/null +++ b/sale_stock_cancel_restriction/models/stock_warehouse.py @@ -0,0 +1,17 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + +HELP_RESTRICT = """ + If set, will block cancellation of sale orders if any delivery is done. + Otherwise, it will block cancellation when any transfer is done. +""" + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" + + restrict_sale_cancel_after_delivery = fields.Boolean( + help=HELP_RESTRICT, default=False + ) diff --git a/sale_stock_cancel_restriction/static/description/index.html b/sale_stock_cancel_restriction/static/description/index.html index 2d66cff8f4d..76d156a183e 100644 --- a/sale_stock_cancel_restriction/static/description/index.html +++ b/sale_stock_cancel_restriction/static/description/index.html @@ -1,4 +1,3 @@ - diff --git a/sale_stock_cancel_restriction/tests/test_sale_stock_cancel_restriction.py b/sale_stock_cancel_restriction/tests/test_sale_stock_cancel_restriction.py index adc4fccda5b..4f35e329548 100644 --- a/sale_stock_cancel_restriction/tests/test_sale_stock_cancel_restriction.py +++ b/sale_stock_cancel_restriction/tests/test_sale_stock_cancel_restriction.py @@ -13,29 +13,74 @@ def setUpClass(cls): {"name": "Product test", "type": "product"} ) cls.partner = cls.env["res.partner"].create({"name": "Partner test"}) + cls.warehouse = cls.env.ref("stock.warehouse0") + + @classmethod + def _create_sale_order(cls): so_form = Form(cls.env["sale.order"]) so_form.partner_id = cls.partner with so_form.order_line.new() as soline_form: soline_form.product_id = cls.product soline_form.product_uom_qty = 2 - cls.sale_order = so_form.save() - cls.sale_order.action_confirm() - cls.picking = cls.sale_order.picking_ids - cls.picking.move_ids.quantity_done = 2 + sale_order = so_form.save() + sale_order.action_confirm() + return sale_order def test_cancel_sale_order_restrict(self): """Validates the picking and do the assertRaises cancelling the order for checking that it's forbidden """ - self.picking.button_validate() + sale_order = self._create_sale_order() + picking = sale_order.picking_ids + picking.move_ids.quantity_done = 2 + picking.button_validate() + with self.assertRaises(UserError): + sale_order.action_cancel() + + def test_cancel_sale_order_restrict_undelivered_picked(self): + # Enable restrict_sale_cancel_after_delivery, and multi step delivery. + # Cancel should be blocked only once picking is delivered + self.warehouse.restrict_sale_cancel_after_delivery = True + self.warehouse.delivery_steps = "pick_ship" + sale_order = self._create_sale_order() + pick_picking = sale_order.picking_ids.filtered( + lambda p: p.picking_type_id.code == "internal" + ) + pick_picking.move_ids.quantity_done = 2 + pick_picking.button_validate() + wizz = sale_order.action_cancel() + self.assertEqual( + wizz["res_model"], + "sale.order.cancel", + ) + + def test_cancel_sale_order_restrict_undelivered_shipped(self): + # Enable restrict_sale_cancel_after_delivery, and multi step delivery. + # Cancel should be blocked only once picking is delivered + self.warehouse.restrict_sale_cancel_after_delivery = True + self.warehouse.delivery_steps = "pick_ship" + sale_order = self._create_sale_order() + # Pick 2 units + pick_picking = sale_order.picking_ids.filtered( + lambda p: p.picking_type_id.code == "internal" + ) + pick_picking.move_ids.quantity_done = 2 + pick_picking.button_validate() + # Deliver 2 units + ship_picking = sale_order.picking_ids.filtered( + lambda p: p.picking_type_id.code == "outgoing" + ) + ship_picking.move_ids.quantity_done = 2 + ship_picking.button_validate() with self.assertRaises(UserError): - self.sale_order.action_cancel() + sale_order.action_cancel() def test_cancel_sale_order_ok(self): """When canceling the order, the wizard is generated with the model 'sale.order.cancel """ - wizz = self.sale_order.action_cancel() + sale_order = self._create_sale_order() + wizz = sale_order.action_cancel() self.assertEqual( wizz["res_model"], "sale.order.cancel", diff --git a/sale_stock_cancel_restriction/views/stock_warehouse.xml b/sale_stock_cancel_restriction/views/stock_warehouse.xml new file mode 100644 index 00000000000..2037fc4cf58 --- /dev/null +++ b/sale_stock_cancel_restriction/views/stock_warehouse.xml @@ -0,0 +1,17 @@ + + + + + + stock.warehouse.form.inherit + stock.warehouse + + + + + + + + +