From 56bd545d8f086889f1e3c6a8763aee5eb8814331 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 | 34 +++++++- .../tests/test_reservation.py | 81 +++++++++++++++++++ 2 files changed, 114 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..683cc25138 100644 --- a/stock_available_to_promise_release/models/stock_move.py +++ b/stock_available_to_promise_release/models/stock_move.py @@ -562,12 +562,16 @@ def _run_stock_rule(self): # Pull the released moves for move in released_moves: + qty_to_release = move._get_qty_to_release() + rounding = move.product_uom.rounding + if float_compare(qty_to_release, 0, precision_rounding=rounding) <= 0: + continue move._before_release() values = move._prepare_procurement_values() 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 +587,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)