[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
This commit is contained in:
Michael Tietz
2024-07-30 14:31:13 +02:00
parent 81e65888db
commit 9b0476ce7b
6 changed files with 91 additions and 15 deletions

View File

@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record
id="job_function_product_product_moves_auto_release"
id="job_function_product_product_pickings_auto_release"
model="queue.job.function"
>
<field name="model_id" ref="product.model_product_product" />
<field name="method">moves_auto_release</field>
<field name="method">pickings_auto_release</field>
<field name="channel_id" ref="channel_stock_auto_release" />
</record>
<record id="job_function_stock_picking_auto_release" model="queue.job.function">
<field name="model_id" ref="stock.model_stock_picking" />
<field name="method">auto_release_available_to_promise</field>
<field name="channel_id" ref="channel_stock_auto_release" />
<field name="retry_pattern" eval="{1: 1, 5: 5, 10: 10, 15: 30}" />
</record>
</odoo>

View File

@@ -1,4 +1,5 @@
# Copyright 2022 ACSONE SA/NV
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
@@ -14,16 +15,15 @@ class ProductProduct(models.Model):
("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()

View File

@@ -1,4 +1,5 @@
# Copyright 2022 ACSONE SA/NV
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
@@ -69,6 +70,6 @@ class StockMove(models.Model):
)
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

View File

@@ -1,9 +1,12 @@
# Copyright 2022 ACSONE SA/NV
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
# 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 @@ class StockPicking(models.Model):
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

View File

@@ -1 +1,2 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* Michael Tietz (MT Software) <mtietz@mt-software.de>

View File

@@ -1,4 +1,5 @@
# Copyright 2022 ACSONE SA/NV
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import datetime
@@ -82,7 +83,7 @@ class TestAssignAutoRelease(PromiseReleaseCommonCase):
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 @@ class TestAssignAutoRelease(PromiseReleaseCommonCase):
self.assertEqual(5, self.picking.move_ids.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_ids))
self.assertEqual(10, self.picking.move_ids.product_qty)
@@ -114,7 +121,7 @@ class TestAssignAutoRelease(PromiseReleaseCommonCase):
)
# 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(
@@ -126,7 +133,7 @@ class TestAssignAutoRelease(PromiseReleaseCommonCase):
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)
@@ -165,3 +172,48 @@ class TestAssignAutoRelease(PromiseReleaseCommonCase):
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()
trap.perform_enqueued_jobs()
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()
trap.perform_enqueued_jobs()
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)