From 40da141611732e7c3d776a9d9b5a5cb9b455c282 Mon Sep 17 00:00:00 2001
From: Jacques-Etienne Baudoux <je@bcim.be>
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 <je@bcim.be>
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) <je@bcim.be>
+# 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) <je@bcim.be>
 * Guewen Baconnier <guewen.baconnier@camptocamp.com>
 * `Trobz <https://trobz.com>`_:
   * Dung Tran <dungtd@trobz.com>

From b07ff5f7307aa51b6ac886b052e86d7d984af87d Mon Sep 17 00:00:00 2001
From: Mmequignon <matthieu@fwzte.xyz>
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)