From 40da141611732e7c3d776a9d9b5a5cb9b455c282 Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Fri, 1 Sep 2023 19:30:01 +0200 Subject: [PATCH 1/3] [FIX] stock_available_to_promise_release Do not assign done or canceled moves --- stock_available_to_promise_release/models/stock_move.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stock_available_to_promise_release/models/stock_move.py b/stock_available_to_promise_release/models/stock_move.py index 7cc514c538..e832cc7745 100644 --- a/stock_available_to_promise_release/models/stock_move.py +++ b/stock_available_to_promise_release/models/stock_move.py @@ -486,8 +486,11 @@ def _after_release_update_chain(self): def _after_release_assign_moves(self): move_ids = [] for origin_moves in self._get_chained_moves_iterator("move_orig_ids"): - move_ids += origin_moves.ids - self.env["stock.move"].browse(move_ids)._action_assign() + move_ids += origin_moves.filtered( + lambda m: m.state not in ("cancel", "done") + ).ids + moves = self.browse(move_ids) + moves._action_assign() def _release_split(self, remaining_qty): """Split move and put remaining_qty to a backorder move.""" From f6c141c411aa804dbbdcd13d291059731f05cc7b Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Fri, 1 Sep 2023 19:30:52 +0200 Subject: [PATCH 2/3] [FIX] stock_dynamic_routing: merge moves Merge reclassified moves after release --- .../models/stock_move.py | 7 ++-- .../__init__.py | 1 + .../__manifest__.py | 3 +- .../models/__init__.py | 1 + .../models/stock_move.py | 36 +++++++++++++++++++ .../readme/CONTRIBUTORS.rst | 1 + 6 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 stock_available_to_promise_release_dynamic_routing/models/__init__.py create mode 100644 stock_available_to_promise_release_dynamic_routing/models/stock_move.py diff --git a/stock_available_to_promise_release/models/stock_move.py b/stock_available_to_promise_release/models/stock_move.py index e832cc7745..65fe8953f2 100644 --- a/stock_available_to_promise_release/models/stock_move.py +++ b/stock_available_to_promise_release/models/stock_move.py @@ -462,10 +462,10 @@ def _run_stock_rule(self): ) self.env["procurement.group"].run_defer(procurement_requests) - released_moves._after_release_assign_moves() - released_moves._after_release_update_chain() + assigned_moves = released_moves._after_release_assign_moves() + assigned_moves._after_release_update_chain() - return released_moves + return assigned_moves def _before_release(self): """Hook that aims to be overridden.""" @@ -491,6 +491,7 @@ def _after_release_assign_moves(self): ).ids moves = self.browse(move_ids) moves._action_assign() + return moves def _release_split(self, remaining_qty): """Split move and put remaining_qty to a backorder move.""" diff --git a/stock_available_to_promise_release_dynamic_routing/__init__.py b/stock_available_to_promise_release_dynamic_routing/__init__.py index e69de29bb2..0650744f6b 100644 --- a/stock_available_to_promise_release_dynamic_routing/__init__.py +++ b/stock_available_to_promise_release_dynamic_routing/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_available_to_promise_release_dynamic_routing/__manifest__.py b/stock_available_to_promise_release_dynamic_routing/__manifest__.py index 3b4ea353c8..93a72f1657 100644 --- a/stock_available_to_promise_release_dynamic_routing/__manifest__.py +++ b/stock_available_to_promise_release_dynamic_routing/__manifest__.py @@ -3,7 +3,8 @@ { "name": "Available to Promise Release - Dynamic Routing", "summary": "Glue between moves release and dynamic routing", - "author": "Camptocamp, Odoo Community Association (OCA)", + "author": "Camptocamp,BCIM,Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], "website": "https://github.com/OCA/wms", "category": "Warehouse Management", "version": "14.0.1.0.0", diff --git a/stock_available_to_promise_release_dynamic_routing/models/__init__.py b/stock_available_to_promise_release_dynamic_routing/models/__init__.py new file mode 100644 index 0000000000..6bda2d2428 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/models/__init__.py @@ -0,0 +1 @@ +from . import stock_move diff --git a/stock_available_to_promise_release_dynamic_routing/models/stock_move.py b/stock_available_to_promise_release_dynamic_routing/models/stock_move.py new file mode 100644 index 0000000000..f34887d52d --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/models/stock_move.py @@ -0,0 +1,36 @@ +# Copyright 2023 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from itertools import groupby + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _after_release_assign_moves(self): + # Trigger the dynamic routing + moves = super()._after_release_assign_moves() + # Check if moves can be merged. We do this after the call to + # _action_assign in super as this could delete some records in self + sorted_moves_by_rule = sorted(moves, key=lambda m: m.picking_id.id) + moves_to_rereserve_ids = [] + new_moves = self.browse() + for _picking_id, move_list in groupby( + sorted_moves_by_rule, key=lambda m: m.picking_id.id + ): + moves = self.browse(m.id for m in move_list) + merged_moves = moves._merge_moves() + new_moves |= merged_moves + if moves != merged_moves: + for move in merged_moves: + if not move.quantity_done: + moves_to_rereserve_ids.append(move.id) + if moves_to_rereserve_ids: + moves_to_rereserve = self.browse(moves_to_rereserve_ids) + moves_to_rereserve._do_unreserve() + moves_to_rereserve.with_context( + exclude_apply_dynamic_routing=True + )._action_assign() + return new_moves diff --git a/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst b/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst index 9d9e83e703..ca56f7d4c6 100644 --- a/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst +++ b/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ +* Jacques-Etienne Baudoux (BCIM) * Guewen Baconnier * `Trobz `_: * Dung Tran From b07ff5f7307aa51b6ac886b052e86d7d984af87d Mon Sep 17 00:00:00 2001 From: Mmequignon Date: Mon, 11 Sep 2023 15:14:10 +0200 Subject: [PATCH 3/3] stock_available_to_promise_release: Fix move unreleasing --- .../models/stock_move.py | 5 ++ .../tests/__init__.py | 1 + .../tests/test_unrelease_merged_moves.py | 54 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 stock_available_to_promise_release/tests/test_unrelease_merged_moves.py diff --git a/stock_available_to_promise_release/models/stock_move.py b/stock_available_to_promise_release/models/stock_move.py index 65fe8953f2..d91232268d 100644 --- a/stock_available_to_promise_release/models/stock_move.py +++ b/stock_available_to_promise_release/models/stock_move.py @@ -579,6 +579,8 @@ def _split_origins(self, origins): """ self.ensure_one() qty = self.product_qty + # Unreserve goods before the split + origins._do_unreserve() rounding = self.product_uom.rounding new_origin_moves = self.env["stock.move"] while float_compare(qty, 0, precision_rounding=rounding) > 0 and origins: @@ -591,6 +593,9 @@ def _split_origins(self, origins): new_origin_moves |= self.create(new_move_vals) break origins -= origin + # And then do the reservation again + origins._action_assign() + new_origin_moves._action_assign() return new_origin_moves def _search_picking_for_assignation_domain(self): diff --git a/stock_available_to_promise_release/tests/__init__.py b/stock_available_to_promise_release/tests/__init__.py index b29b6d9eb4..1eee41662b 100644 --- a/stock_available_to_promise_release/tests/__init__.py +++ b/stock_available_to_promise_release/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_unrelease from . import test_unrelease_2steps from . import test_unrelease_3steps +from . import test_unrelease_merged_moves diff --git a/stock_available_to_promise_release/tests/test_unrelease_merged_moves.py b/stock_available_to_promise_release/tests/test_unrelease_merged_moves.py new file mode 100644 index 0000000000..199363835d --- /dev/null +++ b/stock_available_to_promise_release/tests/test_unrelease_merged_moves.py @@ -0,0 +1,54 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from datetime import datetime + +from .common import PromiseReleaseCommonCase + + +class TestAvailableToPromiseRelease(PromiseReleaseCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + delivery_pick_rule = cls.wh.delivery_route_id.rule_ids.filtered( + lambda r: r.location_src_id == cls.loc_stock + ) + delivery_pick_rule.group_propagation_option = "fixed" + cls.pc1 = cls._create_picking_chain( + cls.wh, [(cls.product1, 2)], date=datetime(2019, 9, 2, 16, 0) + ) + cls.shipping1 = cls._out_picking(cls.pc1) + cls.pc2 = cls._create_picking_chain( + cls.wh, [(cls.product1, 3)], date=datetime(2019, 9, 2, 16, 0) + ) + cls.shipping2 = cls._out_picking(cls.pc2) + cls._update_qty_in_location(cls.loc_bin1, cls.product1, 15.0) + cls.wh.delivery_route_id.write( + { + "available_to_promise_defer_pull": True, + } + ) + shippings = cls.shipping1 | cls.shipping2 + shippings.release_available_to_promise() + cls.picking1 = cls._prev_picking(cls.shipping1) + cls.picking1.action_assign() + cls.picking2 = cls._prev_picking(cls.shipping2) + cls.picking2.action_assign() + + @classmethod + def _out_picking(cls, pickings): + return pickings.filtered(lambda r: r.picking_type_code == "outgoing") + + @classmethod + def _prev_picking(cls, picking): + return picking.move_lines.move_orig_ids.picking_id + + def test_unrelease_merged_move(self): + self.assertEqual(self.picking1, self.picking2) + moves = self.picking1.move_lines.filtered(lambda m: m.state == "assigned") + self.assertEqual(sum(moves.mapped("product_uom_qty")), 5.0) + self.shipping2.unrelease() + move = self.picking1.move_lines.filtered(lambda m: m.state == "assigned") + line = move.move_line_ids + self.assertEqual(move.product_uom_qty, 2.0) + self.assertEqual(line.product_uom_qty, 2.0)