From 6eb9c8f95a9979f46728dc2432a79680f70015fe Mon Sep 17 00:00:00 2001 From: Michael Tietz Date: Tue, 30 Jul 2024 14:31:13 +0200 Subject: [PATCH] [IMP] stock_move_auto_assign_auto_release: Release release_ready pickings instead of moves Instead of just releasing the release ready moves of a give product it now releases the whole transfer This ensures that a transfer with a release_policy=one gets not split --- .../data/queue_job_function_data.xml | 11 +++- .../models/product_product.py | 12 ++-- .../models/stock_move.py | 3 +- .../models/stock_picking.py | 19 +++++- .../readme/CONTRIBUTORS.rst | 1 + .../tests/test_assign_auto_release.py | 66 +++++++++++++++++-- 6 files changed, 97 insertions(+), 15 deletions(-) diff --git a/stock_move_auto_assign_auto_release/data/queue_job_function_data.xml b/stock_move_auto_assign_auto_release/data/queue_job_function_data.xml index 1b01bdfaee9d..64663429ca50 100644 --- a/stock_move_auto_assign_auto_release/data/queue_job_function_data.xml +++ b/stock_move_auto_assign_auto_release/data/queue_job_function_data.xml @@ -1,12 +1,17 @@ - moves_auto_release + pickings_auto_release + + + + + + auto_release_available_to_promise - diff --git a/stock_move_auto_assign_auto_release/models/product_product.py b/stock_move_auto_assign_auto_release/models/product_product.py index d099250cdbbe..18386daf1882 100644 --- a/stock_move_auto_assign_auto_release/models/product_product.py +++ b/stock_move_auto_assign_auto_release/models/product_product.py @@ -1,4 +1,5 @@ # Copyright 2022 ACSONE SA/NV +# Copyright 2024 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import models @@ -14,16 +15,15 @@ def _moves_auto_release_domain(self): ("is_auto_release_allowed", "=", True), ] - def moves_auto_release(self): - """Job trying to auto release moves based on product + def pickings_auto_release(self): + """Job trying to auto release pickings based on product - It searches all* the moves auto releasable and trigger the release - available to promise process. + It searches all* the moves auto releasable + and triggers a delayed release available to promise for their pickings. """ self.ensure_one() moves = self.env["stock.move"].search(self._moves_auto_release_domain()) pickings = moves.picking_id if not pickings: return - self._lock_pickings_or_retry(pickings) - moves.release_available_to_promise() + pickings._delay_auto_release_available_to_promise() diff --git a/stock_move_auto_assign_auto_release/models/stock_move.py b/stock_move_auto_assign_auto_release/models/stock_move.py index 33836980a6fe..38415202bb23 100644 --- a/stock_move_auto_assign_auto_release/models/stock_move.py +++ b/stock_move_auto_assign_auto_release/models/stock_move.py @@ -1,4 +1,5 @@ # Copyright 2022 ACSONE SA/NV +# Copyright 2024 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models @@ -67,6 +68,6 @@ def _enqueue_auto_assign(self, product, locations, **job_options): ) job_options.setdefault("identity_key", identity_exact) delayable = product.delayable(**job_options) - release_job = delayable.moves_auto_release() + release_job = delayable.pickings_auto_release() job.on_done(release_job) return job diff --git a/stock_move_auto_assign_auto_release/models/stock_picking.py b/stock_move_auto_assign_auto_release/models/stock_picking.py index 8b40daf6f1c4..1d63d5bcac5b 100644 --- a/stock_move_auto_assign_auto_release/models/stock_picking.py +++ b/stock_move_auto_assign_auto_release/models/stock_picking.py @@ -1,9 +1,12 @@ # Copyright 2022 ACSONE SA/NV +# Copyright 2024 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.osv.expression import NEGATIVE_TERM_OPERATORS +from odoo.addons.queue_job.job import identity_exact + class StockPicking(models.Model): @@ -47,3 +50,17 @@ def _search_is_auto_release_allowed(self, operator, value): if not is_auto_release_allowed: domain = [("id", "not in", self.search(domain).ids)] return domain + + def _delay_auto_release_available_to_promise(self): + for picking in self: + picking.with_delay( + identity_key=identity_exact, + description=_( + "Auto release available to promise %(name)s", name=picking.name + ), + ).auto_release_available_to_promise() + + def auto_release_available_to_promise(self): + to_release = self.filtered("is_auto_release_allowed") + to_release.release_available_to_promise() + return to_release diff --git a/stock_move_auto_assign_auto_release/readme/CONTRIBUTORS.rst b/stock_move_auto_assign_auto_release/readme/CONTRIBUTORS.rst index 172b2d223cef..fd6329eb2ea9 100644 --- a/stock_move_auto_assign_auto_release/readme/CONTRIBUTORS.rst +++ b/stock_move_auto_assign_auto_release/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Laurent Mignon +* Michael Tietz (MT Software) diff --git a/stock_move_auto_assign_auto_release/tests/test_assign_auto_release.py b/stock_move_auto_assign_auto_release/tests/test_assign_auto_release.py index 614af12ffd1a..46db1231f7db 100644 --- a/stock_move_auto_assign_auto_release/tests/test_assign_auto_release.py +++ b/stock_move_auto_assign_auto_release/tests/test_assign_auto_release.py @@ -1,4 +1,5 @@ # Copyright 2022 ACSONE SA/NV +# Copyright 2024 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import datetime @@ -82,7 +83,7 @@ def _receive_product(self, product=None, qty=None): move.move_line_ids.location_dest_id = self.loc_bin1.id move._action_done() - def test_product_moves_auto_release(self): + def test_product_pickings_auto_release(self): """Test job method, update qty available and launch auto release on the product""" self.assertEqual(1, len(self.unreleased_move)) @@ -90,7 +91,13 @@ def test_product_moves_auto_release(self): self.assertEqual(5, self.picking.move_lines.product_qty) # put stock in Stock/Shelf 1, the move has a source location in Stock self._update_qty_in_location(self.loc_bin1, self.product1, 100) - self.product1.moves_auto_release() + with trap_jobs() as trap: + self.product1.pickings_auto_release() + job = self._get_job_for_method( + trap.enqueued_jobs, + self.unreleased_move.picking_id.auto_release_available_to_promise, + ) + job.perform() self.assertFalse(self.unreleased_move.need_release) self.assertEqual(1, len(self.picking.move_lines)) self.assertEqual(10, self.picking.move_lines.product_qty) @@ -115,7 +122,7 @@ def test_move_done_enqueue_job(self): ) # and a second one to auto release trap.assert_enqueued_job( - self.product1.moves_auto_release, + self.product1.pickings_auto_release, args=(), kwargs={}, properties=dict( @@ -127,7 +134,7 @@ def test_move_done_enqueue_job(self): trap.enqueued_jobs, self.product1.moves_auto_assign ) job2 = self._get_job_for_method( - trap.enqueued_jobs, self.product1.moves_auto_release + trap.enqueued_jobs, self.product1.pickings_auto_release ) self.assertIn(job1, job2.depends_on) @@ -166,3 +173,54 @@ def test_move_field_is_auto_release_allowed(self): for domain in NOT_RELEASABLE_DOMAINS: self.assertIn(move_released, self.env["stock.move"].search(domain)) self.assertNotIn(move_not_released, self.env["stock.move"].search(domain)) + + def test_picking_policy_one_async_receive(self): + self.shipping.action_cancel() + self.picking.action_cancel() + shipping = self._out_picking( + self._create_picking_chain( + self.wh, + [(self.product1, 10), (self.product2, 10)], + date=datetime(2019, 9, 2, 16, 0), + ) + ) + shipping.release_policy = "one" + shipping.move_type = "one" + self.assertTrue( + all( + move.need_release and not move.release_ready + for move in shipping.move_lines + ) + ) + with trap_jobs() as trap: + self._receive_product(self.product1, 100) + shipping.invalidate_cache() + shipping.move_lines.invalidate_cache() + jobs = trap.enqueued_jobs + with trap_jobs() as trap: + for job in jobs: + job.perform() + job = self._get_job_for_method( + trap.enqueued_jobs, shipping.auto_release_available_to_promise + ) + self.assertFalse(job) + with trap_jobs() as trap: + self._receive_product(self.product2, 100) + shipping.invalidate_cache() + shipping.move_lines.invalidate_cache() + jobs = trap.enqueued_jobs + with trap_jobs() as trap: + for job in jobs: + job.perform() + job = self._get_job_for_method( + trap.enqueued_jobs, shipping.auto_release_available_to_promise + ) + job.perform() + move_product1 = shipping.move_lines.filtered( + lambda m: m.product_id == self.product1 + ) + move_product2 = shipping.move_lines - move_product1 + self.assertFalse(move_product2.release_ready) + self.assertFalse(move_product2.need_release) + self.assertFalse(move_product1.need_release) + self.assertFalse(move_product1.release_ready)