Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions sale_order_line_cancel/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
{
"name": "Sale Order Line Cancel",
"version": "16.0.1.3.1",
"author": "Okia, BCIM, Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
"author": "Okia, BCIM, Camptocamp, ACSONE SA/NV, "
"MT Software, Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Sales",
"summary": """Sale cancel remaining""",
"depends": ["sale_stock"],
"depends": ["sale"],
"data": [
"security/sale_order_line_cancel.xml",
"wizards/sale_order_line_cancel.xml",
Expand Down
1 change: 0 additions & 1 deletion sale_order_line_cancel/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from . import sale_order_line
from . import stock_move
from . import sale_order
from . import res_company
from . import res_config_settings
8 changes: 1 addition & 7 deletions sale_order_line_cancel/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2023 ACSONE SA/NV
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


Expand All @@ -13,9 +13,3 @@ def action_draft(self):
orders = self.filtered(lambda s: s.state == "draft")
orders.order_line.write({"product_qty_canceled": 0})
return res

def _action_cancel(self):
new_self = self.with_context(ignore_sale_order_line_cancel=True)
res = super(SaleOrder, new_self)._action_cancel()
self.order_line._update_qty_canceled()
return res
39 changes: 13 additions & 26 deletions sale_order_line_cancel/models/sale_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright 2020 ACSONE SA/NV
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import _, api, fields, models
from odoo.tools import float_compare

Expand All @@ -30,35 +29,25 @@ def _compute_can_cancel_remaining_qty(self):
"Product Unit of Measure"
)
for rec in self:
rec.can_cancel_remaining_qty = (
float_compare(
rec.product_qty_remains_to_deliver, 0, precision_digits=precision
)
== 1
and rec.state in ("sale", "done")
and rec.qty_delivered_method == "stock_move"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is now gone, imo it is not needed to check for qty_delivered_method.

)
rec.can_cancel_remaining_qty = float_compare(
rec.product_qty_remains_to_deliver, 0, precision_digits=precision
) == 1 and rec.state in ("sale", "done")

@api.depends("qty_to_deliver", "product_qty_canceled")
@api.depends(
"product_uom_qty",
"qty_delivered",
"product_qty_canceled",
)
def _compute_product_qty_remains_to_deliver(self):
for line in self:
qty_remaining = max(0, line.qty_to_deliver - line.product_qty_canceled)
qty_to_deliver = line.product_uom_qty - line.qty_delivered
qty_remaining = max(0, qty_to_deliver - line.product_qty_canceled)
line.product_qty_remains_to_deliver = qty_remaining

def _get_moves_to_cancel(self):
lines = self.filtered(lambda l: l.qty_delivered_method == "stock_move")
return lines.move_ids.filtered(lambda m: m.state not in ("done", "cancel"))

def _check_moves_to_cancel(self, moves):
"""Override this method to add checks before cancel"""
self.ensure_one()

def _update_qty_canceled(self):
"""Update SO line qty canceled only when all remaining moves are canceled"""
for line in self:
if line._get_moves_to_cancel():
continue
qty_to_deliver = line.qty_to_deliver
qty_to_deliver = line.product_uom_qty - line.qty_delivered
vals = {"product_qty_canceled": qty_to_deliver}
if (
line.state == "sale"
Expand All @@ -69,14 +58,12 @@ def _update_qty_canceled(self):

def cancel_remaining_qty(self):
lines = self.filtered(lambda l: l.can_cancel_remaining_qty)
lines._update_qty_canceled()
for line in lines:
moves_to_cancel = line._get_moves_to_cancel()
line._check_moves_to_cancel(moves_to_cancel)
moves_to_cancel._action_cancel()
line.order_id.message_post(
body=_(
"<b>%(product)s</b>: The order line has been canceled",
product=line.product_id.display_name,
)
)
return True
return lines
1 change: 1 addition & 0 deletions sale_order_line_cancel/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
* Sylvain Van Hoof <sylvain@okia.be>
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu.com>
* Laurent Mignon <laurent.mignon@acsone.eu>
* Michael Tietz (MT Software) <mtietz@mt-software.de>
13 changes: 2 additions & 11 deletions sale_order_line_cancel/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,5 @@ This module differs from the original odoo behavior in the following way:
the confirmed state, odoo will recompute the required stock operations
according to the new quantity. This change is possible
even the stock operations are started for this sale order line.
* In this module, the quantity ordered is not updated on the sale order line to
keep track of the original ordered by the customer. At the same time, we
cancel only the stock moves for the remaining qty to deliver. This is only
possible if no operation is started for this sale order line.


.. warning::

It's not recommended to use this module if the update of the quantity ordered
on the sale order line is allowed the confirmed state. This could lead to
unpredictable behavior.
* In this module, you can either decide if only the canceled quantity gets tracked
or if it also should decrease the original ordered quantity.
69 changes: 34 additions & 35 deletions sale_order_line_cancel/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2023 ACSONE SA/NV
# Copyright 2025 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import Command
Expand All @@ -10,54 +11,52 @@ class TestSaleOrderLineCancelBase(TransactionCase):
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env["res.partner"].create({"name": "Partner"})
cls.warehouse = cls.env.ref("stock.warehouse0")
cls.product_1 = cls.env["product.product"].create(
{
"name": "test product 1",
"type": "product",
"sale_ok": True,
"active": True,
}
)
cls.product_2 = cls.product_1.copy({"name": "test product 2"})
cls.product_3 = cls.product_1.copy({"name": "test product 3"})
cls.product_1 = cls._create_product()
cls.sale = cls._add_done_sale_order()
cls.sale.action_done()
cls.wiz = cls.env["sale.order.line.cancel"].create({})
cls.env["stock.quant"]._update_available_quantity(
cls.product_1, cls.warehouse.lot_stock_id, 10.0
)

@classmethod
def _add_done_sale_order(
cls, partner=None, product=None, qty=10, picking_policy="direct"
):
if partner is None:
partner = cls.partner
if product is None:
product = cls.product_1
warehouse = cls.warehouse
sale_order_model = cls.env["sale.order"]
def _prepare_product_vals(cls):
return {
"name": "test product 1",
"sale_ok": True,
"active": True,
}

@classmethod
def _create_product(cls):
return cls.env["product.product"].create(cls._prepare_product_vals())

@classmethod
def _prepare_sale_order_values(cls, **kwargs):
lines = [
Command.create(
{
"name": p.name,
"product_id": p.id,
"product_uom_qty": qty,
"product_uom": p.uom_id.id,
"name": cls.product_1.name,
"product_id": cls.product_1.id,
"product_uom_qty": 10,
"product_uom": cls.product_1.uom_id.id,
"price_unit": 1,
},
}
)
for p in product
]
so_values = {
"partner_id": partner.id,
"warehouse_id": warehouse.id,
"partner_id": cls.partner.id,
"order_line": lines,
}
if picking_policy:
so_values["picking_policy"] = picking_policy
so = sale_order_model.create(so_values)
if kwargs:
so_values.update(kwargs)
return so_values

@classmethod
def _create_sale_order(cls, **kwargs):
sale_order_model = cls.env["sale.order"]
so_values = cls._prepare_sale_order_values(**kwargs)
return sale_order_model.create(so_values)

@classmethod
def _add_done_sale_order(cls, **kwargs):
so = cls._create_sale_order(**kwargs)
so.action_confirm()
so.action_done()
return so
Loading