Skip to content

Commit

Permalink
Add compatibility with stock_dynamic_routing
Browse files Browse the repository at this point in the history
in stock_move_common_dest_sync_location

Some notes:

* syncing the destinations could not be done during _action_assign: if
we call _action_done on several moves at once with different locations,
we don't expect them to change of destination under the hood! So we sync
after _action_done
* For now, no code is needed in stock_dynamic_routing_common_dest_sync
to make the modules compatibles, but it contains tests with routing +
sync
  • Loading branch information
guewen committed Apr 30, 2020
1 parent 745b8fb commit 93e448f
Show file tree
Hide file tree
Showing 12 changed files with 387 additions and 58 deletions.
6 changes: 6 additions & 0 deletions setup/stock_dynamic_routing_common_dest_sync/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
1 change: 1 addition & 0 deletions stock_dynamic_routing_common_dest_sync/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
20 changes: 20 additions & 0 deletions stock_dynamic_routing_common_dest_sync/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
{
"name": "Stock Routing Operations - Dest. Sync",
"summary": "Glue module",
"author": "Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"category": "Warehouse Management",
"version": "13.0.1.0.0",
"license": "AGPL-3",
"depends": [
# FIXME will be renamed to stock_dynamic_routing
"stock_routing_operation",
"stock_move_common_dest_sync_location",
],
"demo": [],
"data": [],
"auto_install": True,
"installable": True,
"development_status": "Alpha",
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <[email protected]>
5 changes: 5 additions & 0 deletions stock_dynamic_routing_common_dest_sync/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Glue module between ``stock_move_common_dest_sync_location`` and
``stock_dynamic_routing``.

Currently, the module only contains tests to verify the compatibility
between these two modules, but compatibility code may be needed later.
1 change: 1 addition & 0 deletions stock_dynamic_routing_common_dest_sync/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_routing_sync
208 changes: 208 additions & 0 deletions stock_dynamic_routing_common_dest_sync/tests/test_routing_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)

from odoo.addons.stock_move_common_dest_sync_location.tests.test_move_common_dest_sync_location import ( # noqa
TestCommonSyncDest,
)


class TestRoutingPullWithSync(TestCommonSyncDest):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.location_pack_load = cls.env["stock.location"].create(
{"name": "Packing Load", "location_id": cls.packing_location.id}
)
cls.location_pack_post = cls.env["stock.location"].create(
{"name": "Packing Post", "location_id": cls.packing_location.id}
)
cls.location_pack_post_bay1 = cls.env["stock.location"].create(
{"name": "Bay 1", "location_id": cls.location_pack_post.id}
)
cls.pack_post_type = cls.pack_type.copy(
{
"name": "Packing Post",
"sequence_code": "WH/POST",
"default_location_src_id": cls.location_pack_post.id,
}
)
cls.routing = cls.env["stock.routing"].create(
{
"location_id": cls.location_pack_post.id,
"rule_ids": [
(0, 0, {"method": "pull", "picking_type_id": cls.pack_post_type.id})
],
}
)
cls._update_qty_in_location(cls.stock_shelf_location, cls.product_1, 10)
cls._update_qty_in_location(cls.stock_shelf_location, cls.product_2, 10)

cls.pick_move1 = cls._create_single_move(cls.pick_type, cls.product_1)
cls.pack_move1 = cls._create_single_move(
cls.pack_type, cls.product_1, move_orig=cls.pick_move1
)
cls.pick_move2 = cls._create_single_move(cls.pick_type, cls.product_2)
cls.pack_move2 = cls._create_single_move(
cls.pack_type, cls.product_2, move_orig=cls.pick_move2
)
cls.pick_move3 = cls._create_single_move(cls.pick_type, cls.product_2)
cls.pack_move3 = cls._create_single_move(
cls.pack_type, cls.product_2, move_orig=cls.pick_move3
)
moves = (
cls.pick_move1
+ cls.pack_move1
+ cls.pick_move2
+ cls.pack_move2
+ cls.pick_move3
+ cls.pack_move3
)
moves._assign_picking()
moves._action_assign()

def assert_picking_type_pack(self, record):
self.assertEqual(record.picking_type_id, self.pack_type)

def assert_picking_type_pack_post(self, record):
self.assertEqual(record.picking_type_id, self.pack_post_type)

def assert_src_packing(self, record):
self.assertEqual(record.location_id, self.packing_location)

def assert_dest_packing(self, record):
self.assertEqual(record.location_dest_id, self.packing_location)

def assert_src_pack_post(self, record):
self.assertEqual(record.location_id, self.location_pack_post)

def assert_src_pack_post_bay1(self, record):
self.assertEqual(record.location_id, self.location_pack_post_bay1)

def assert_src_pack_load(self, record):
self.assertEqual(record.location_id, self.location_pack_load)

def assert_dest_pack_post(self, record):
self.assertEqual(record.location_dest_id, self.location_pack_post)

def assert_dest_pack_post_bay1(self, record):
self.assertEqual(record.location_dest_id, self.location_pack_post_bay1)

def assert_dest_pack_load(self, record):
self.assertEqual(record.location_dest_id, self.location_pack_load)

def test_pack_sync(self):
self.pack_type.sync_common_move_dest_location = True
self.pack_post_type.sync_common_move_dest_location = True

self.pick_move1.move_line_ids.write(
{
"location_dest_id": self.location_pack_post_bay1.id,
"qty_done": self.pick_move1.move_line_ids.product_uom_qty,
}
)
self.pick_move1._action_done()

self.assert_dest_pack_post(self.pick_move1)
self.assert_dest_pack_post_bay1(self.pick_move1.move_line_ids)
self.assert_dest_pack_post(self.pick_move1)
self.assert_dest_pack_post_bay1(self.pick_move2.move_line_ids)
self.assert_dest_pack_post(self.pick_move3)
self.assert_dest_pack_post_bay1(self.pick_move3.move_line_ids)

self.assert_src_pack_post(self.pack_move1)
self.assert_src_pack_post_bay1(self.pack_move1.move_line_ids)
# no move lines on these waiting moves:
self.assert_src_pack_post(self.pack_move2)
self.assert_src_pack_post(self.pack_move3)

self.assert_picking_type_pack_post(self.pack_move1.picking_id)
self.assert_picking_type_pack_post(self.pack_move2.picking_id)
self.assert_picking_type_pack_post(self.pack_move3.picking_id)

def test_pack_sync_all_at_once(self):
self.pack_type.sync_common_move_dest_location = True
self.pack_post_type.sync_common_move_dest_location = True

self.pick_move1.move_line_ids.write(
{
"location_dest_id": self.location_pack_post_bay1.id,
"qty_done": self.pick_move1.move_line_ids.product_uom_qty,
}
)
self.pick_move2.move_line_ids.write(
{
"location_dest_id": self.location_pack_post_bay1.id,
"qty_done": self.pick_move2.move_line_ids.product_uom_qty,
}
)
self.pick_move3.move_line_ids.write(
{
"location_dest_id": self.location_pack_load.id,
"qty_done": self.pick_move3.move_line_ids.product_uom_qty,
}
)
# done on all the picking at once: we expect the original destinations
# to be kept
self.pick_move1.picking_id.action_done()

self.assert_dest_pack_post(self.pick_move2)
self.assert_dest_pack_post_bay1(self.pick_move2.move_line_ids)
self.assert_dest_packing(self.pick_move3)
self.assert_dest_pack_load(self.pick_move3.move_line_ids)

self.assert_src_pack_post(self.pack_move1)
self.assert_src_pack_post_bay1(self.pack_move1.move_line_ids)
self.assert_src_pack_post(self.pack_move2)
self.assert_src_pack_post_bay1(self.pack_move1.move_line_ids)
self.assert_src_packing(self.pack_move3)
self.assert_src_pack_load(self.pack_move3.move_line_ids)

self.assert_picking_type_pack_post(self.pack_move1.picking_id)
self.assert_picking_type_pack_post(self.pack_move2.picking_id)
# remains on the Pack picking type because it was moved to a
# different location
self.assert_picking_type_pack(self.pack_move3.picking_id)

def test_pack_sync_split(self):
self.pack_type.sync_common_move_dest_location = True
self.pack_post_type.sync_common_move_dest_location = True

self.pick_move1._do_unreserve()
self.env["stock.quant"].search(
[
("location_id", "=", self.stock_shelf_location.id),
("product_id", "=", self.product_1.id),
]
).unlink()

self._update_qty_in_location(self.stock_shelf_location, self.product_1, 1)

self.pick_move1._action_assign()
self.pick_move1.move_line_ids.write(
{"location_dest_id": self.location_pack_post_bay1.id, "qty_done": 1}
)
self.pick_move1._action_done()
pick_move_split = self.pick_move1.move_dest_ids.move_orig_ids - self.pick_move1
pack_move_split = pick_move_split.move_dest_ids - self.pack_move1
self.assertEqual(pick_move_split.state, "confirmed")
self.assertEqual(self.pack_move1.state, "waiting")
self.assertEqual(pack_move_split.state, "assigned")

self.assert_dest_pack_post(self.pick_move1)
self.assert_dest_pack_post_bay1(self.pick_move1.move_line_ids)
self.assert_dest_pack_post(pick_move_split)
self.assert_dest_pack_post(self.pick_move1)
self.assert_dest_pack_post_bay1(self.pick_move2.move_line_ids)
self.assert_dest_pack_post(self.pick_move3)
self.assert_dest_pack_post_bay1(self.pick_move3.move_line_ids)

self.assert_src_pack_post(pack_move_split)
self.assert_src_pack_post_bay1(pack_move_split.move_line_ids)
# no move lines on these waiting moves:
self.assert_src_pack_post(self.pack_move1)
self.assert_src_pack_post(self.pack_move2)
self.assert_src_pack_post(self.pack_move3)

self.assert_picking_type_pack_post(self.pack_move1.picking_id)
self.assert_picking_type_pack_post(self.pack_move2.picking_id)
self.assert_picking_type_pack_post(self.pack_move3.picking_id)
90 changes: 64 additions & 26 deletions stock_move_common_dest_sync_location/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,79 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import models
from odoo import fields, models


class StockMove(models.Model):
_inherit = "stock.move"

def _action_assign(self):
unassigned = self.filtered(
lambda m: m.state not in ("assigned", "partially_available", "done")
)
super()._action_assign()
unassigned.filtered(
lambda m: m.state in ("assigned", "partially_available")
)._sync_same_destination_orig_moves()
def _action_done(self, cancel_backorder=False):
to_sync = {}
for move in self:
# store the original moves that goes in the same transfer, because
# when we call super(), changes made to the chain of moves (for
# instance the application of a "dynamic routing" by
# stock_dynamic_routing) may happen.
if move.move_dest_ids.filtered(self._filter_sync_destination):
to_sync[move] = move.common_dest_move_ids

# When using dynamic routing, it will be applied applied during the
# call to super. The routing can move a stock.move to another
# stock.picking, insert a new stock.move...
# We have to apply the synchronization of the destinations once the
# move is done to be sure it was really the selected destination, and a
# user started to move goods at this place.
moves_todo = super()._action_done(cancel_backorder=cancel_backorder)

for move, neighbours in to_sync.items():
if move.state != "done":
continue

# if any new move were added (split, extra move, ...) we have to
# synchronize their destination location as well
neighbours |= move.common_dest_move_ids

# find the location where the neighbour moves (eg. the moves which
# have to be packed together, so moved at the same place) amongst
# the destination moves. We consider only assigned moves, which
# means part of the goods have been moved there.
dest_move = move.move_dest_ids.filtered(
lambda m: m.state in ("assigned", "partially_available")
and self._filter_sync_destination(m)
)
dest_move = fields.first(dest_move)
dest_move_line = fields.first(dest_move.move_line_ids)

# Sync the destinations to group the moves in the same
# location. If a routing was applied to the assigned move,
# the other waiting moves will now match the same routing
# which will be applied.
move._sync_destination_to_neighbour_moves(
neighbours, dest_move.location_id, dest_move_line.location_id
)
return moves_todo

@staticmethod
def _filter_sync_destination(move):
return move.picking_id.picking_type_id.sync_common_move_dest_location

def _sync_same_destination_orig_moves(self):
moves = self.filtered(self._filter_sync_destination)
for move in moves.mapped("move_orig_ids"):
neighbour_moves = move.common_dest_move_ids
move._sync_destination_to_neighbour_moves(neighbour_moves)

def _sync_destination_to_neighbour_moves(self, neighbour_moves):
def _sync_destination_to_neighbour_moves(
self, neighbour_moves, move_destination, move_line_destination
):
self.ensure_one()
# get the destination of the first move which was done, we want all the other
# moves, available or not, so all the goods will be moved to the same place
destination = self.move_line_ids.mapped("location_dest_id")
# moves destination locations are restricted to the same destination,
# so user can't bring goods elsewhere than the good already moved
neighbour_moves.filtered(
lambda m: m.location_dest_id != destination and m.state != "done"
).write({"location_dest_id": destination.id})
neighbour_moves = neighbour_moves.filtered(lambda m: m.state != "done")
# Normally the move destination does not change. But when using other
# addons, such as stock_dynamic_routing, the source location of the
# destination move can change, so handle this case too. (there is a
# glue module stock_dynamic_routing_common_dest_sync).
moves_to_update = (self | neighbour_moves).filtered(
lambda m: m.location_dest_id != move_destination
)
moves_to_update.write({"location_dest_id": move_destination.id})
# Sync the source of the destination move too, if it's still waiting.
moves_to_update.move_dest_ids.filtered(lambda m: m.state == "waiting").write(
{"location_id": move_destination.id}
)
lines = neighbour_moves.mapped("move_line_ids")
lines.filtered(
lambda l: l.location_dest_id != destination and l.state != "done"
).write({"location_dest_id": destination.id})
lambda l: l.location_dest_id != move_line_destination and l.state != "done"
).write({"location_dest_id": move_line_destination.id})
2 changes: 2 additions & 0 deletions stock_move_common_dest_sync_location/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ Works best when used with ``stock_available_to_promise_release``:
is handled by canceling the remaining line.
* When the module is not used, then the destination of the backorders may be
changed at the same time

Compatible with ``stock_dynamic_routing``.
Loading

0 comments on commit 93e448f

Please sign in to comment.