diff --git a/stock_move_auto_assign/models/stock_move.py b/stock_move_auto_assign/models/stock_move.py index 1979d8da5..cdb85dc32 100644 --- a/stock_move_auto_assign/models/stock_move.py +++ b/stock_move_auto_assign/models/stock_move.py @@ -4,6 +4,7 @@ from collections import defaultdict from odoo import _, models +from odoo.tools import float_compare from odoo.addons.queue_job.job import identity_exact @@ -13,10 +14,29 @@ class StockMove(models.Model): def _action_done(self, cancel_backorder=False): done_moves = super()._action_done(cancel_backorder=cancel_backorder) - done_moves._prepare_auto_assign() + done_moves._prepare_auto_assign(location_field="move_line_ids.location_dest_id") return done_moves - def _prepare_auto_assign(self): + def _action_cancel(self): + moves_with_reservation_to_cancel = self.filtered( + lambda m: m.state not in ("done", "cancel") + and float_compare( + m.reserved_availability, 0, precision_rounding=m.product_uom.rounding + ) + > 0 + ) + res = super()._action_cancel() + moves_with_reservation_canceled = ( + self.filtered(lambda m: m.state == "cancel") + & moves_with_reservation_to_cancel + ) + if moves_with_reservation_canceled: + moves_with_reservation_canceled._prepare_auto_assign( + location_field="location_id" + ) + return res + + def _prepare_auto_assign(self, location_field): product_locs = defaultdict(set) for move in self: # select internal locations where we moved goods not for the @@ -26,7 +46,7 @@ class StockMove(models.Model): product = move.product_id if product.type != "product": continue - locations = move.mapped("move_line_ids.location_dest_id").filtered( + locations = move.mapped(location_field).filtered( lambda l: l.usage == "internal" ) product_locs[product.id].update(locations.ids) diff --git a/stock_move_auto_assign/tests/test_auto_assign.py b/stock_move_auto_assign/tests/test_auto_assign.py index 7c2bc7d66..c12ba6e83 100644 --- a/stock_move_auto_assign/tests/test_auto_assign.py +++ b/stock_move_auto_assign/tests/test_auto_assign.py @@ -43,6 +43,35 @@ class TestStockMoveAutoAssign(StockMoveAutoAssignCase): self.assertEqual(delay_args, (self.shelf1_loc | self.shelf2_loc,)) self.assertDictEqual(delay_kwargs, {}) + def test_move_canceled_with_reservation_enqueue_job(self): + """A canceled move with reservations enqueue a new job to assign other moves""" + move = self._create_move(self.product, self.out_type, qty=100) + # put stock in Stock/Shelf 1, the move has a source location in Stock + self._update_qty_in_location(self.shelf1_loc, self.product, 100) + move._action_assign() + with mock_with_delay() as (delayable_cls, delayable): + move._action_cancel() + # .with_delay() has been called once + self.assertEqual(delayable_cls.call_count, 1) + delay_args, delay_kwargs = delayable_cls.call_args + # .with_delay() is called on self.product + self.assertEqual(delay_args, (self.product,)) + # .with_delay() with the following options + self.assertEqual(delay_kwargs.get("identity_key"), identity_exact) + # check what's passed to the job method 'moves_auto_assign' + self.assertEqual(delayable.moves_auto_assign.call_count, 1) + delay_args, delay_kwargs = delayable.moves_auto_assign.call_args + self.assertEqual(delay_args, (self.out_type.default_location_src_id,)) + self.assertDictEqual(delay_kwargs, {}) + + def test_move_canceled_without_reservation_no_job(self): + move = self._create_move(self.product, self.out_type, qty=100) + move._action_assign() + with mock_with_delay() as (delayable_cls, delayable): + move._action_cancel() + # .with_delay() has not been called + self.assertEqual(delayable_cls.call_count, 0) + def test_move_done_service_no_job(self): """Service products do not enqueue job""" self.product.type = "service"