From 581644f7f79ba8657fd467daf7cdd220efec7a8c Mon Sep 17 00:00:00 2001 From: "Laurent Mignonn (ACSONE)" Date: Thu, 17 Oct 2024 15:11:10 +0200 Subject: [PATCH] [IMP] stock_available_to_promise_release: Improved release process robustness Previous to this change, if the release process was 'forced' multiple time on an already released move, the system issued new procumrement rules without thaking into account the qty already released or processed. As side effect, the qty into the picking operation was higher than the expected qty to deliver. We now take into account the qty already released into the release process --- .../models/stock_move.py | 30 ++++++- .../tests/test_reservation.py | 81 +++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/stock_available_to_promise_release/models/stock_move.py b/stock_available_to_promise_release/models/stock_move.py index d59d90eb59..16ec7a1902 100644 --- a/stock_available_to_promise_release/models/stock_move.py +++ b/stock_available_to_promise_release/models/stock_move.py @@ -567,7 +567,7 @@ def _run_stock_rule(self): procurement_requests.append( self.env["procurement.group"].Procurement( move.product_id, - move.product_uom_qty, + move._get_qty_to_release(), move.product_uom, move.location_id, move.rule_id and move.rule_id.name or "/", @@ -583,6 +583,34 @@ def _run_stock_rule(self): return released_moves + def _get_qty_to_release(self): + """Return the qty to release for the move + + The qty to release is the move qty minus the qty released for this move + minus the the qty already reserved for the move. + + This qty will never exceed the ordered available to promise qty. + """ + self.ensure_one() + released_moves = self.move_orig_ids.filtered( + lambda m: m.state not in ("done", "cancel") + ) + all_released_qty = sum(released_moves.mapped("product_uom_qty")) + others_requesting_moves = ( + released_moves.move_dest_ids.filtered( + lambda m: m.state not in ("done", "cancel") + ) + - self + ) + others_moves_requested_qty = sum( + others_requesting_moves.mapped("product_uom_qty") + ) - sum(others_requesting_moves.mapped("reserved_availability")) + current_released_qty = all_released_qty - others_moves_requested_qty + to_release = ( + self.product_uom_qty - self.reserved_availability - current_released_qty + ) + return min(to_release, self.ordered_available_to_promise_qty) + def _before_release(self): """Hook that aims to be overridden.""" self._release_set_expected_date() diff --git a/stock_available_to_promise_release/tests/test_reservation.py b/stock_available_to_promise_release/tests/test_reservation.py index 11891f61db..8d45c1eefe 100644 --- a/stock_available_to_promise_release/tests/test_reservation.py +++ b/stock_available_to_promise_release/tests/test_reservation.py @@ -1242,3 +1242,84 @@ def test_release_policy(self): picking.release_available_to_promise() new_picking = self._pickings_in_group(picking.group_id) - picking self.assertEqual(new_picking.move_type, "one") + + def test_release_available_multiple_calls(self): + self.wh.delivery_route_id.write({"available_to_promise_defer_pull": True}) + # put some qty in output location + self._update_qty_in_location(self.wh.lot_stock_id, self.product1, 5.0) + ship = self._create_picking_chain(self.wh, [(self.product1, 10)]) + + ship.release_available_to_promise() + pick_pick = ship.move_ids.move_orig_ids.picking_id + self.assertEqual(pick_pick.move_ids.product_uom_qty, 5.0) + + ship_backorder = ship.backorder_ids + self.assertTrue(ship_backorder) + self.assertEqual(ship_backorder.move_ids.product_uom_qty, 5.0) + self.assertFalse(ship_backorder.move_ids.move_orig_ids.picking_id) + ships = ship + ship_backorder + + # the same call to release_available_to_promise should not create a new picking + # nor change the qty of the existing one + ships.release_available_to_promise() + self.assertEqual(pick_pick, ship.move_ids.move_orig_ids.picking_id) + self.assertEqual(pick_pick.move_ids.product_uom_qty, 5.0) + self.assertEqual(ship_backorder.move_ids.product_uom_qty, 5.0) + self.assertFalse(ship_backorder.move_ids.move_orig_ids.picking_id) + + # put more qty in output location + # and force release + self._update_qty_in_location(self.wh.lot_stock_id, self.product1, 10.0) + ships.move_ids.need_release = True + + # the release should update the qty of the existing picking to the new qty + # available + ships.release_available_to_promise() + self.assertEqual(pick_pick, ship.move_ids.move_orig_ids.picking_id) + self.assertEqual(pick_pick.move_ids.product_uom_qty, 10.0) + self.assertEqual(ship_backorder.move_ids.move_orig_ids.picking_id, pick_pick) + + # partially process the picking + pick_pick.action_assign() + pick_pick.move_line_ids.qty_done = 3.0 + pick_pick._action_done() + + # the pick should still contain the remaining qty + pick_pick = ship.move_ids.move_orig_ids.filtered( + lambda p: p.state not in ("done", "cancel") + ).picking_id + self.assertEqual(pick_pick.move_ids.product_uom_qty, 7.0) + + # force release again + ship.move_ids.need_release = True + ship.release_available_to_promise() + + # release should take into account already processed qty + self.assertEqual(pick_pick.move_ids.product_uom_qty, 7.0) + + # force release of backorder + ship_backorder.move_ids.need_release = True + ship_backorder.release_available_to_promise() + self.assertEqual(pick_pick.move_ids.product_uom_qty, 7.0) + + # if we release the two ships at same time, it's without effect + ships.move_ids.need_release = True + ships.release_available_to_promise() + self.assertEqual(pick_pick.move_ids.product_uom_qty, 7.0) + + # if we cancel the remaining pick and release again, the new + # picking must be for the remaining qty + pick_pick.action_cancel() + ship.move_ids.need_release = True + ship.release_available_to_promise() + + pick_pick = ship.move_ids.move_orig_ids.picking_id.filtered( + lambda p: p.state not in ("done", "cancel") + ) + # only the first picking is released -> 5.0 - 3.0 = 2.0 + self.assertEqual(pick_pick.move_ids.product_uom_qty, 2.0) + + ship_backorder.move_ids.need_release = True + ship_backorder.release_available_to_promise() + # the backorder is for all the remaining qty + self.assertEqual(pick_pick.move_ids.product_uom_qty, 7.0)