Add stock_move_auto_assign

Automatically check availability of stock moves when a move is set to "done".

It uses queue jobs to verify the availability in order to have a minimal impact
on the user operations.

The conditions to trigger the check are:

* A move is marked as done
* The destination locations of the move lines are internal
* The move doesn't have successors in a chain of moves

At this point, jobs are generated:

* One job per product
* Any move waiting for stock in a parent (or same) location of the internal
  destination locations from the done move has its availability checked

Only one job is generated for an identical set of (product, locations).
This commit is contained in:
Guewen Baconnier
2020-06-12 16:12:12 +02:00
committed by Hai Lang
parent 9ae15b66ce
commit d617c4ea4d
13 changed files with 824 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests import SavepointCase
from odoo.addons.queue_job.job import identity_exact
from odoo.addons.queue_job.tests.common import mock_with_delay
class TestStockMoveAutoAssign(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.wh = cls.env.ref("stock.warehouse0")
cls.out_type = cls.wh.out_type_id
cls.in_type = cls.wh.in_type_id
cls.int_type = cls.wh.int_type_id
cls.customer_loc = cls.env.ref("stock.stock_location_customers")
cls.supplier_loc = cls.env.ref("stock.stock_location_suppliers")
cls.shelf1_loc = cls.env.ref("stock.stock_location_components")
cls.shelf2_loc = cls.env.ref("stock.stock_location_14")
cls.product = cls.env["product.product"].create(
{"name": "Product", "type": "product"}
)
def _create_move(
self,
product,
picking_type,
qty=1.0,
state="confirmed",
procure_method="make_to_stock",
move_dest=None,
):
source = picking_type.default_location_src_id or self.supplier_loc
dest = picking_type.default_location_dest_id or self.customer_loc
move_vals = {
"name": product.name,
"product_id": product.id,
"product_uom_qty": qty,
"product_uom": product.uom_id.id,
"picking_type_id": picking_type.id,
"location_id": source.id,
"location_dest_id": dest.id,
"state": state,
"procure_method": procure_method,
}
if move_dest:
move_vals["move_dest_ids"] = [(4, move_dest.id, False)]
return self.env["stock.move"].create(move_vals)
def _update_qty_in_location(self, location, product, quantity):
self.env["stock.quant"]._update_available_quantity(product, location, quantity)
def test_job_assign_confirmed_move(self):
"""Test job method, assign moves matching product and location"""
move1 = self._create_move(self.product, self.out_type)
move2 = self._create_move(self.product, self.out_type)
# put stock in Stock/Shelf 1, the move has a source location in Stock
self._update_qty_in_location(self.shelf1_loc, self.product, 100)
self.product.moves_auto_assign(self.shelf1_loc)
self.assertEqual(move1.state, "assigned")
self.assertEqual(move2.state, "assigned")
def test_move_done_enqueue_job(self):
"""A move done enqueue a new job to assign other moves"""
move = self._create_move(self.product, self.in_type, qty=100)
move._action_assign()
move.move_line_ids.qty_done = 50
move.move_line_ids.location_dest_id = self.shelf1_loc.id
move.move_line_ids.copy(
default={"qty_done": 50, "location_dest_id": self.shelf2_loc.id}
)
with mock_with_delay() as (delayable_cls, delayable):
move._action_done()
# .with_delay() has been called once
self.assertEqual(delayable_cls.call_count, 1)
delay_args, delay_kwargs = delayable_cls.call_args
# .with_delay() is called on self.product
self.assertEqual(delay_args, (self.product,))
# .with_delay() with the following options
self.assertEqual(delay_kwargs.get("identity_key"), identity_exact)
# check what's passed to the job method 'moves_auto_assign'
self.assertEqual(delayable.moves_auto_assign.call_count, 1)
delay_args, delay_kwargs = delayable.moves_auto_assign.call_args
self.assertEqual(delay_args, (self.shelf1_loc | self.shelf2_loc,))
self.assertDictEqual(delay_kwargs, {})
def test_move_done_service_no_job(self):
"""Service products do not enqueue job"""
self.product.type = "service"
move = self._create_move(self.product, self.in_type, qty=1)
move._action_assign()
move.move_line_ids.qty_done = 1
move.move_line_ids.location_dest_id = self.shelf1_loc.id
with mock_with_delay() as (delayable_cls, delayable):
move._action_done()
# .with_delay() has not been called
self.assertEqual(delayable_cls.call_count, 0)
def test_move_done_chained_no_job(self):
"""A move chained to another does not enqueue job"""
move_out = self._create_move(
self.product, self.out_type, qty=1, state="waiting"
)
move = self._create_move(self.product, self.in_type, qty=1, move_dest=move_out)
move._action_assign()
move.move_line_ids.qty_done = 1
move.move_line_ids.location_dest_id = self.shelf1_loc.id
with mock_with_delay() as (delayable_cls, delayable):
move._action_done()
# .with_delay() has not been called
self.assertEqual(delayable_cls.call_count, 0)
def test_move_done_customer_no_job(self):
"""A move with other destination than internal does not enqueue job"""
move = self._create_move(self.product, self.out_type, qty=1)
self._update_qty_in_location(self.shelf1_loc, self.product, 1)
move._action_assign()
move.move_line_ids.qty_done = 1
move.move_line_ids.location_dest_id = self.customer_loc
with mock_with_delay() as (delayable_cls, delayable):
move._action_done()
# .with_delay() has not been called
self.assertEqual(delayable_cls.call_count, 0)