diff --git a/sale_order_line_cancel/__manifest__.py b/sale_order_line_cancel/__manifest__.py index 91002adc450..c86db022eb6 100644 --- a/sale_order_line_cancel/__manifest__.py +++ b/sale_order_line_cancel/__manifest__.py @@ -1,4 +1,5 @@ -# © 2016 Sylvain Van Hoof +# Copyright 2018 Sylvain Van Hoof (Okia SPRL) +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) # Copyright 2023 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). diff --git a/sale_order_line_cancel/models/sale_order_line.py b/sale_order_line_cancel/models/sale_order_line.py index 8d72a8c6563..119d6ab8cd5 100644 --- a/sale_order_line_cancel/models/sale_order_line.py +++ b/sale_order_line_cancel/models/sale_order_line.py @@ -1,8 +1,9 @@ # Copyright 2018 Okia SPRL +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) # Copyright 2020 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.tools import float_compare @@ -34,7 +35,7 @@ def _compute_can_cancel_remaining_qty(self): ) == 1 and rec.state in ("sale", "done") - and rec.move_ids + and rec.qty_delivered_method == "stock_move" ) @api.depends( @@ -44,10 +45,33 @@ def _compute_can_cancel_remaining_qty(self): ) def _compute_product_qty_remains_to_deliver(self): for line in self: - remaining_to_deliver = ( - line.product_uom_qty - line.qty_delivered - line.product_qty_canceled - ) - line.product_qty_remains_to_deliver = remaining_to_deliver + qty_remaining = line.qty_to_deliver - line.product_qty_canceled + line.product_qty_remains_to_deliver = qty_remaining + + def _get_moves_to_cancel(self): + return self.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""" + """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 + line.product_qty_canceled = line.qty_to_deliver + + def cancel_remaining_qty(self): + lines = self.filtered(lambda l: l.can_cancel_remaining_qty) + 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=_( + "%(product)s: The order line has been canceled", + product=line.product_id.display_name, + ) + ) + return True diff --git a/sale_order_line_cancel/models/stock_move.py b/sale_order_line_cancel/models/stock_move.py index 80167aa9cc1..461150927f3 100644 --- a/sale_order_line_cancel/models/stock_move.py +++ b/sale_order_line_cancel/models/stock_move.py @@ -1,4 +1,5 @@ # Copyright 2023 ACSONE SA/NV +# Copyright 2024 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import models @@ -13,8 +14,14 @@ def _action_cancel(self): lambda m: m.sale_line_id and m.state not in ("done", "cancel") ) res = super()._action_cancel() - for rec in sale_moves: - if rec.state != "cancel": - continue - rec.sale_line_id.product_qty_canceled = rec.product_uom_qty + sale_lines = sale_moves.filtered(lambda m: m.state == "cancel").sale_line_id + sale_lines._update_qty_canceled() return res + + def _action_done(self, cancel_backorder=False): + moves_todo = super()._action_done(cancel_backorder=cancel_backorder) + if cancel_backorder and moves_todo: + # _action_cancel is called before marking as done, so the hook on + # _action_cancel will not be triggered. Call it now + self.sale_line_id._update_qty_canceled() + return moves_todo diff --git a/sale_order_line_cancel/tests/test_sale_order_line_cancel.py b/sale_order_line_cancel/tests/test_sale_order_line_cancel.py index cbd5549d385..5c6c1fd9dbf 100644 --- a/sale_order_line_cancel/tests/test_sale_order_line_cancel.py +++ b/sale_order_line_cancel/tests/test_sale_order_line_cancel.py @@ -60,6 +60,25 @@ def test_cancel_pickings(self): active_id=self.sale.order_line.id, active_model="sale.order.line" ).cancel_remaining_qty() + def test_cancel_move_kit(self): + """when all remaining moves are canceled product_qty_canceled increased""" + self.assertTrue(self.sale.order_line.can_cancel_remaining_qty) + move = self.sale.picking_ids.move_ids + self.assertEqual(move.sale_line_id, self.sale.order_line) + # simulate a kit with a second move linked to the sale SO line + move2 = move.copy() + move2._action_confirm() + self.assertEqual(move2.sale_line_id, self.sale.order_line) + move._action_cancel() + self.assertEqual(self.sale.order_line.product_qty_canceled, 0) + move2._action_cancel() + self.assertEqual(self.sale.order_line.product_qty_canceled, 10) + self.assertEqual(self.sale.order_line.product_qty_remains_to_deliver, 0) + self.assertFalse(self.sale.order_line.can_cancel_remaining_qty) + self.wiz.with_context( + active_id=self.sale.order_line.id, active_model="sale.order.line" + ).cancel_remaining_qty() + def test_reset_to_draft(self): ship = self.sale.picking_ids ship.action_assign() diff --git a/sale_order_line_cancel/wizards/sale_order_line_cancel.py b/sale_order_line_cancel/wizards/sale_order_line_cancel.py index 5aef7c611ea..0a256e65c43 100644 --- a/sale_order_line_cancel/wizards/sale_order_line_cancel.py +++ b/sale_order_line_cancel/wizards/sale_order_line_cancel.py @@ -23,15 +23,5 @@ def _get_moves_to_cancel(self, line): def cancel_remaining_qty(self): line = self._get_sale_order_line() - if not line.can_cancel_remaining_qty: - return False - cancel_moves = self._get_moves_to_cancel(line) - line._check_moves_to_cancel(cancel_moves) - cancel_moves._action_cancel() - line.order_id.message_post( - body=_( - "%(product)s: The order line has been canceled", - product=line.product_id.display_name, - ) - ) + line.cancel_remaining_qty() return True