Skip to content

Commit 6655af7

Browse files
[IMP] sale_order_line_cancel: Adapt code to work with base addon
1 parent 7894873 commit 6655af7

16 files changed

+48
-343
lines changed

sale_order_line_cancel/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
from . import models
2-
from . import wizards
3-
from .hooks import pre_init_hook

sale_order_line_cancel/__manifest__.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,12 @@
77
{
88
"name": "Sale Order Line Cancel",
99
"version": "16.0.1.3.1",
10-
"author": "Okia, BCIM, Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
10+
"author": "Okia, BCIM, Camptocamp, ACSONE SA/NV, "
11+
"MT Software, Odoo Community Association (OCA)",
1112
"license": "AGPL-3",
1213
"category": "Sales",
1314
"summary": """Sale cancel remaining""",
14-
"depends": ["sale_stock"],
15-
"data": [
16-
"security/sale_order_line_cancel.xml",
17-
"wizards/sale_order_line_cancel.xml",
18-
"views/sale_order.xml",
19-
"views/sale_order_line.xml",
20-
"views/res_config_settings_views.xml",
21-
],
15+
"depends": ["sale_order_line_cancel_base", "sale_stock"],
16+
"data": [],
2217
"website": "https://github.com/OCA/sale-workflow",
23-
"pre_init_hook": "pre_init_hook",
2418
}

sale_order_line_cancel/hooks.py

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
from . import sale_order_line
2-
from . import stock_move
32
from . import sale_order
4-
from . import res_company
5-
from . import res_config_settings
3+
from . import stock_move
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
# Copyright 2023 ACSONE SA/NV
2+
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
23
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3-
44
from odoo import models
55

66

77
class SaleOrder(models.Model):
8-
98
_inherit = "sale.order"
109

11-
def action_draft(self):
12-
res = super().action_draft()
13-
orders = self.filtered(lambda s: s.state == "draft")
14-
orders.order_line.write({"product_qty_canceled": 0})
15-
return res
16-
1710
def _action_cancel(self):
1811
new_self = self.with_context(ignore_sale_order_line_cancel=True)
1912
res = super(SaleOrder, new_self)._action_cancel()
20-
self.order_line._update_qty_canceled()
2113
return res

sale_order_line_cancel/models/sale_order_line.py

Lines changed: 12 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,79 +4,27 @@
44
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
55
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
66

7-
from odoo import _, api, fields, models
8-
from odoo.tools import float_compare
7+
from odoo import models
98

109

1110
class SaleOrderLine(models.Model):
1211
_inherit = "sale.order.line"
1312

14-
product_qty_canceled = fields.Float(
15-
"Qty canceled", readonly=True, copy=False, digits="Product Unit of Measure"
16-
)
17-
product_qty_remains_to_deliver = fields.Float(
18-
string="Remains to deliver",
19-
digits="Product Unit of Measure",
20-
compute="_compute_product_qty_remains_to_deliver",
21-
store=True,
22-
)
23-
can_cancel_remaining_qty = fields.Boolean(
24-
compute="_compute_can_cancel_remaining_qty"
25-
)
26-
27-
@api.depends("product_qty_remains_to_deliver", "state")
28-
def _compute_can_cancel_remaining_qty(self):
29-
precision = self.env["decimal.precision"].precision_get(
30-
"Product Unit of Measure"
31-
)
32-
for rec in self:
33-
rec.can_cancel_remaining_qty = (
34-
float_compare(
35-
rec.product_qty_remains_to_deliver, 0, precision_digits=precision
36-
)
37-
== 1
38-
and rec.state in ("sale", "done")
39-
and rec.qty_delivered_method == "stock_move"
40-
)
41-
42-
@api.depends("qty_to_deliver", "product_qty_canceled")
43-
def _compute_product_qty_remains_to_deliver(self):
44-
for line in self:
45-
qty_remaining = max(0, line.qty_to_deliver - line.product_qty_canceled)
46-
line.product_qty_remains_to_deliver = qty_remaining
47-
4813
def _get_moves_to_cancel(self):
49-
lines = self.filtered(lambda l: l.qty_delivered_method == "stock_move")
14+
lines = self.filtered(
15+
lambda l: l.qty_delivered_method == "stock_move"
16+
and l.can_cancel_remaining_qty
17+
)
5018
return lines.move_ids.filtered(lambda m: m.state not in ("done", "cancel"))
5119

5220
def _check_moves_to_cancel(self, moves):
5321
"""Override this method to add checks before cancel"""
54-
self.ensure_one()
55-
56-
def _update_qty_canceled(self):
57-
"""Update SO line qty canceled only when all remaining moves are canceled"""
58-
for line in self:
59-
if line._get_moves_to_cancel():
60-
continue
61-
qty_to_deliver = line.qty_to_deliver
62-
vals = {"product_qty_canceled": qty_to_deliver}
63-
if (
64-
line.state == "sale"
65-
and line.company_id.on_sale_line_cancel_decrease_line_qty
66-
):
67-
vals["product_uom_qty"] = line.qty_delivered
68-
line.write(vals)
6922

7023
def cancel_remaining_qty(self):
71-
lines = self.filtered(lambda l: l.can_cancel_remaining_qty)
72-
for line in lines:
73-
moves_to_cancel = line._get_moves_to_cancel()
74-
line._check_moves_to_cancel(moves_to_cancel)
75-
moves_to_cancel._action_cancel()
76-
line.order_id.message_post(
77-
body=_(
78-
"<b>%(product)s</b>: The order line has been canceled",
79-
product=line.product_id.display_name,
80-
)
81-
)
82-
return True
24+
moves_to_cancel = self._get_moves_to_cancel()
25+
moves_to_cancel.sale_line_id._check_moves_to_cancel(moves_to_cancel)
26+
if moves_to_cancel:
27+
moves_to_cancel.with_context(
28+
ignore_sale_order_line_cancel=True
29+
)._action_cancel()
30+
return super().cancel_remaining_qty()

sale_order_line_cancel/models/stock_move.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,17 @@ def _action_done(self, cancel_backorder=False):
2929
def _get_sale_lines_to_update_qty_canceled(self):
3030
sale_lines = self.env["sale.order.line"]
3131
for move in self:
32-
if (
33-
move.sale_line_id
34-
and move._is_move_to_take_into_account_for_qty_canceled()
35-
):
32+
if move._is_move_to_take_into_account_for_qty_canceled():
3633
sale_lines |= move.sale_line_id
3734
return sale_lines
3835

3936
def _is_move_to_take_into_account_for_qty_canceled(self):
4037
self.ensure_one()
38+
sale_line = self.sale_line_id
4139
return (
42-
self.state == "cancel"
43-
and self.sale_line_id
44-
and self.sale_line_id.state not in ["draft", "sent"]
40+
sale_line
41+
and self.state == "cancel"
4542
and self.picking_type_id.code == "outgoing"
43+
and sale_line.state not in ["draft", "sent", "cancel"]
44+
and not sale_line._get_moves_to_cancel()
4645
)

sale_order_line_cancel/readme/DESCRIPTION.rst

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
This module allows you to cancel the remaining quantity on sale order by adding
2-
a dedicated action to sale lines. It also add two new fields to track canceled
3-
and remaining to deliver quantities.
1+
This module cancels only the stock moves for the remaining qty to deliver.
2+
Also it will track the canceled qty if a order line's stock move is canceled
3+
but only if there are not other started operations for this sale order line.
44

5-
This module differs from the original odoo behavior in the following way:
6-
7-
* In odoo, if the update of the quantity ordered is allowed on the sale order at
8-
the confirmed state, odoo will recompute the required stock operations
9-
according to the new quantity. This change is possible
10-
even the stock operations are started for this sale order line.
11-
* In this module, the quantity ordered is not updated on the sale order line to
12-
keep track of the original ordered by the customer. At the same time, we
13-
cancel only the stock moves for the remaining qty to deliver. This is only
14-
possible if no operation is started for this sale order line.
5+
When the base addon is configured to also decrease the original ordered qty
6+
it ensures that there are now new moves created. Because by default,
7+
odoo will recompute the required stock operations if the ordered qty is changed.
8+
By canceling the operations for the remaining qty before the ordered qty is changed,
9+
odoo will not recompute the required stock operations, because the qty done by moves
10+
is the same as the ordered qty.
1511

1612

1713
.. warning::

sale_order_line_cancel/security/sale_order_line_cancel.xml

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,28 @@
11
# Copyright 2023 ACSONE SA/NV
2+
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
23
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from odoo.addons.sale_order_line_cancel_base.tests.common import (
5+
TestSaleOrderLineCancelBase as Base,
6+
)
37

4-
from odoo import Command
5-
from odoo.tests.common import TransactionCase
68

7-
8-
class TestSaleOrderLineCancelBase(TransactionCase):
9+
class TestSaleOrderLineCancelBase(Base):
910
@classmethod
1011
def setUpClass(cls):
1112
super().setUpClass()
12-
cls.partner = cls.env["res.partner"].create({"name": "Partner"})
1313
cls.warehouse = cls.env.ref("stock.warehouse0")
14-
cls.product_1 = cls.env["product.product"].create(
15-
{
16-
"name": "test product 1",
17-
"type": "product",
18-
"sale_ok": True,
19-
"active": True,
20-
}
21-
)
22-
cls.product_2 = cls.product_1.copy({"name": "test product 2"})
23-
cls.product_3 = cls.product_1.copy({"name": "test product 3"})
24-
cls.sale = cls._add_done_sale_order()
25-
cls.sale.action_done()
26-
cls.wiz = cls.env["sale.order.line.cancel"].create({})
2714
cls.env["stock.quant"]._update_available_quantity(
2815
cls.product_1, cls.warehouse.lot_stock_id, 10.0
2916
)
3017

3118
@classmethod
32-
def _add_done_sale_order(
33-
cls, partner=None, product=None, qty=10, picking_policy="direct"
34-
):
35-
if partner is None:
36-
partner = cls.partner
37-
if product is None:
38-
product = cls.product_1
39-
warehouse = cls.warehouse
40-
sale_order_model = cls.env["sale.order"]
41-
lines = [
42-
Command.create(
43-
{
44-
"name": p.name,
45-
"product_id": p.id,
46-
"product_uom_qty": qty,
47-
"product_uom": p.uom_id.id,
48-
"price_unit": 1,
49-
},
50-
)
51-
for p in product
52-
]
53-
so_values = {
54-
"partner_id": partner.id,
55-
"warehouse_id": warehouse.id,
56-
"order_line": lines,
57-
}
58-
if picking_policy:
59-
so_values["picking_policy"] = picking_policy
60-
so = sale_order_model.create(so_values)
61-
so.action_confirm()
62-
so.action_done()
63-
return so
19+
def _prepare_product_vals(cls):
20+
vals = super()._prepare_product_vals()
21+
vals["type"] = "product"
22+
return vals
23+
24+
@classmethod
25+
def _add_done_sale_order(cls, **kwargs):
26+
if "picking_policy" not in kwargs:
27+
kwargs["picking_policy"] = "direct"
28+
return super()._add_done_sale_order(**kwargs)

0 commit comments

Comments
 (0)