From d6b34b968f43779b90b387b9c19934709b9db653 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 7 Jul 2020 15:43:51 +0200 Subject: [PATCH 1/2] Add stock_picking_consolidation_priority This module adds an option "Raise priority when partially available" on operation types. When the option is set on an operation type, the first time a packing operation is made partially available (goods have been moved for instance from a picking), all the other moves that will bring goods for the same packing transfer have their priority raised to "Very Urgent". If the moves that bring goods to the packing transfer involves a chain of several moves, all the moves of the chain have their priority raised. --- .../stock_picking_consolidation_priority | 1 + .../setup.py | 6 + .../__init__.py | 1 + .../__manifest__.py | 15 ++ .../models/__init__.py | 2 + .../models/stock_move.py | 118 +++++++++ .../models/stock_picking_type.py | 19 ++ .../readme/CONFIGURATION.rst | 1 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 14 + .../readme/ROADMAP.rst | 3 + .../tests/__init__.py | 1 + .../tests/test_consolidation_priority.py | 249 ++++++++++++++++++ 13 files changed, 431 insertions(+) create mode 120000 setup/stock_picking_consolidation_priority/odoo/addons/stock_picking_consolidation_priority create mode 100644 setup/stock_picking_consolidation_priority/setup.py create mode 100644 stock_picking_consolidation_priority/__init__.py create mode 100644 stock_picking_consolidation_priority/__manifest__.py create mode 100644 stock_picking_consolidation_priority/models/__init__.py create mode 100644 stock_picking_consolidation_priority/models/stock_move.py create mode 100644 stock_picking_consolidation_priority/models/stock_picking_type.py create mode 100644 stock_picking_consolidation_priority/readme/CONFIGURATION.rst create mode 100644 stock_picking_consolidation_priority/readme/CONTRIBUTORS.rst create mode 100644 stock_picking_consolidation_priority/readme/DESCRIPTION.rst create mode 100644 stock_picking_consolidation_priority/readme/ROADMAP.rst create mode 100644 stock_picking_consolidation_priority/tests/__init__.py create mode 100644 stock_picking_consolidation_priority/tests/test_consolidation_priority.py diff --git a/setup/stock_picking_consolidation_priority/odoo/addons/stock_picking_consolidation_priority b/setup/stock_picking_consolidation_priority/odoo/addons/stock_picking_consolidation_priority new file mode 120000 index 000000000..a6884e436 --- /dev/null +++ b/setup/stock_picking_consolidation_priority/odoo/addons/stock_picking_consolidation_priority @@ -0,0 +1 @@ +../../../../stock_picking_consolidation_priority \ No newline at end of file diff --git a/setup/stock_picking_consolidation_priority/setup.py b/setup/stock_picking_consolidation_priority/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_picking_consolidation_priority/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_picking_consolidation_priority/__init__.py b/stock_picking_consolidation_priority/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/stock_picking_consolidation_priority/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_picking_consolidation_priority/__manifest__.py b/stock_picking_consolidation_priority/__manifest__.py new file mode 100644 index 000000000..a809cf136 --- /dev/null +++ b/stock_picking_consolidation_priority/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Stock Transfers Consolidation Priority", + "summary": "Raise priority of all transfers for a chain when started", + "version": "13.0.1.0.0", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "category": "Stock Management", + "depends": ["stock"], + "data": [], + "installable": True, + "license": "AGPL-3", +} diff --git a/stock_picking_consolidation_priority/models/__init__.py b/stock_picking_consolidation_priority/models/__init__.py new file mode 100644 index 000000000..1e2ccbb54 --- /dev/null +++ b/stock_picking_consolidation_priority/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_picking_type +from . import stock_move diff --git a/stock_picking_consolidation_priority/models/stock_move.py b/stock_picking_consolidation_priority/models/stock_move.py new file mode 100644 index 000000000..b8ef3c9fc --- /dev/null +++ b/stock_picking_consolidation_priority/models/stock_move.py @@ -0,0 +1,118 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import models +from odoo.osv import expression + + +class StockMove(models.Model): + _inherit = "stock.move" + + _consolidate_priority_value = "3" + + def _action_done(self, cancel_backorder=False): + moves_to_check = super()._action_done(cancel_backorder=cancel_backorder) + moves_to_check.filtered(lambda move: move.state == "done") + if moves_to_check: + moves_to_check._consolidate_priority() + return moves_to_check + + def _query_get_consolidate_moves(self): + """Return a query to find the moves to consolidate in priority + + Consider this chain of moves:: + + PICK/001 ━► PACK/001 ┓ + ┃ + PICK/002 ┓ ┣━► OUT/001 + ┣━► PACK/002 ┛ + PICK/003 ┛ + + If the flag "consolidate_priority" is set on the picking type of OUT, + when we set any of the PICK to done, the 3 PICK and the 2 PACK must + be returned to have their priority raised. + + If the flag is set on PACK and PICK/002 or PICK/003 is set to done, the + other PICK that leads to PACK/002 must be returned to have its priority + raised. But if only PICK/001 is set to done, nothing is returned. + + If the flag is set on both PACK and OUT, the result is the same as the + first case: all PICK and PACK are returned. + """ + query = """ + WITH RECURSIVE + + -- Walk through all relations in stock_move_move_rel to find the + -- destination moves. For each move, join on stock_picking_type + -- to check if the flag "consolidate_priority" is set + destinations (id, consolidate_origins) AS ( + -- starting move + SELECT stock_move.id, + -- the moves here are done: their origin moves are + -- supposed to be done too, we don't need to raise their + -- priority + false as consolidate_origins + FROM stock_move + WHERE stock_move.id IN %s + UNION + -- recurse to find all the destinations + SELECT move_dest.id, + stock_picking_type.consolidate_priority as consolidate_origins + FROM stock_move move_dest + INNER JOIN stock_move_move_rel + ON move_dest.id = stock_move_move_rel.move_dest_id + INNER JOIN stock_picking + ON stock_picking.id = move_dest.picking_id + INNER JOIN stock_picking_type + ON stock_picking_type.id = stock_picking.picking_type_id + INNER JOIN destinations + ON destinations.id = stock_move_move_rel.move_orig_id + ), + + -- For every destination move for which we have the flag set, walk back + -- through stock_move_move_rel to find all the origin moves + origins (id, consolidate_origins) AS ( + -- for all the destinations which have to consolidate their origins, + -- it finds all the origin moves, in the final query, the rows with + -- "consolidate_origins" equal to true are excluded, because we want + -- to raise the priority of the moves *before* the picking type with + -- the flag + SELECT destinations.id, true + FROM destinations + WHERE consolidate_origins = true + -- We use union here to keep duplicate in case a move both has the flag + -- "consolidate_priority" on its picking type AND is the origin move + -- for another move with the flag. Anyway, the final query filters + -- on the second part of the union. + UNION ALL + -- recurse to find all the origin moves which have a destination that + -- needs priority consolidation + SELECT move_orig.id, false + FROM stock_move move_orig + INNER JOIN stock_move_move_rel + ON move_orig.id = stock_move_move_rel.move_orig_id + INNER JOIN origins + ON origins.id = stock_move_move_rel.move_dest_id + ) + SELECT DISTINCT id FROM origins + WHERE consolidate_origins = false + """ + return (query, (tuple(self.ids),)) + + def _consolidate_priority_domain(self): + return [("state", "not in", ("cancel", "done"))] + + def _consolidate_priority_values(self): + return {"priority": self._consolidate_priority_value} + + def _consolidate_priority(self): + query, params = self._query_get_consolidate_moves() + self.env.cr.execute(query, params) + move_ids = [row[0] for row in self.env.cr.fetchall()] + if not move_ids: + return + moves = self.search( + expression.AND( + [[("id", "in", move_ids)], self._consolidate_priority_domain()] + ) + ) + moves.write(self._consolidate_priority_values()) diff --git a/stock_picking_consolidation_priority/models/stock_picking_type.py b/stock_picking_consolidation_priority/models/stock_picking_type.py new file mode 100644 index 000000000..a0c145796 --- /dev/null +++ b/stock_picking_consolidation_priority/models/stock_picking_type.py @@ -0,0 +1,19 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class StockPickingType(models.Model): + _inherit = "stock.picking.type" + + consolidate_priority = fields.Boolean( + string="Raise priority when partially available", + help="Tick this box to raise the priority of all previous related" + " picking when current transfer will be made partially available." + " This is usually used in packing zone when several people work" + " on different transfers to be consolidated in the packing zone." + " When the first one finish, all other related pickings gets with" + " a high priority. The goal is to reduce the number of order being" + " packed at the same time as the space is often limited.", + default=False, + ) diff --git a/stock_picking_consolidation_priority/readme/CONFIGURATION.rst b/stock_picking_consolidation_priority/readme/CONFIGURATION.rst new file mode 100644 index 000000000..c04bb1dde --- /dev/null +++ b/stock_picking_consolidation_priority/readme/CONFIGURATION.rst @@ -0,0 +1 @@ +Update the flag "Raise priority when partially available" on Operation Types. diff --git a/stock_picking_consolidation_priority/readme/CONTRIBUTORS.rst b/stock_picking_consolidation_priority/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..48286263c --- /dev/null +++ b/stock_picking_consolidation_priority/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guewen Baconnier diff --git a/stock_picking_consolidation_priority/readme/DESCRIPTION.rst b/stock_picking_consolidation_priority/readme/DESCRIPTION.rst new file mode 100644 index 000000000..c4c09fbe8 --- /dev/null +++ b/stock_picking_consolidation_priority/readme/DESCRIPTION.rst @@ -0,0 +1,14 @@ +As the number of packing space is usually limited, if someone starts to prepare +an order by picking it in a zone, all other related picking in the other zones +should be of a higher priority. This should make a "started" order a priority +for all. + +This module adds an option "Raise priority when partially available" on +operation types. + +When the option is set on an operation type, the first time a packing operation +is made partially available (goods have been moved for instance from a picking), +all the other moves that will bring goods for the same packing transfer have +their priority raised to "Very Urgent". If the moves that bring goods to the +packing transfer involves a chain of several moves, all the moves of the chain +have their priority raised. diff --git a/stock_picking_consolidation_priority/readme/ROADMAP.rst b/stock_picking_consolidation_priority/readme/ROADMAP.rst new file mode 100644 index 000000000..7c199c149 --- /dev/null +++ b/stock_picking_consolidation_priority/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* The priority is hardcoded to "Very Urgent", it could be made configurable. + That being said, it's easy enough to override ``_consolidate_priority_value`` + to customize the level. diff --git a/stock_picking_consolidation_priority/tests/__init__.py b/stock_picking_consolidation_priority/tests/__init__.py new file mode 100644 index 000000000..317c70333 --- /dev/null +++ b/stock_picking_consolidation_priority/tests/__init__.py @@ -0,0 +1 @@ +from . import test_consolidation_priority diff --git a/stock_picking_consolidation_priority/tests/test_consolidation_priority.py b/stock_picking_consolidation_priority/tests/test_consolidation_priority.py new file mode 100644 index 000000000..20caacdda --- /dev/null +++ b/stock_picking_consolidation_priority/tests/test_consolidation_priority.py @@ -0,0 +1,249 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from collections import namedtuple + +from odoo.tests.common import SavepointCase + + +class TestConsolidationPriority(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.warehouse.write({"delivery_steps": "pick_pack_ship"}) + cls.stock_shelf_location = cls.env.ref("stock.stock_location_components") + cls.customers_location = cls.env.ref("stock.stock_location_customers") + cls.product = cls.env["product.product"].create( + {"name": "Product", "type": "product"} + ) + + cls.pick_type = cls.warehouse.pick_type_id + cls.pack_type = cls.warehouse.pack_type_id + cls.out_type = cls.warehouse.out_type_id + cls.int_type = cls.warehouse.int_type_id + cls.procurement_group_1 = cls.env["procurement.group"].create( + {"name": "Test 1"} + ) + + cls.chain = cls.build_moves() + + @classmethod + def _update_qty_in_location(cls, location, product, quantity): + cls.env["stock.quant"]._update_available_quantity(product, location, quantity) + + @classmethod + def _create_move_with_picking(cls, picking_type, product, quantity, move_orig=None): + move_vals = { + "name": product.name, + "picking_type_id": picking_type.id, + "product_id": product.id, + "product_uom_qty": 2.0, + "product_uom": product.uom_id.id, + "location_id": picking_type.default_location_src_id.id, + "location_dest_id": picking_type.default_location_dest_id.id + or cls.customers_location.id, + "state": "confirmed", + "procure_method": "make_to_stock", + "group_id": cls.procurement_group_1.id, + } + if move_orig: + move_vals.update( + { + "procure_method": "make_to_order", + "state": "waiting", + "move_orig_ids": [(6, 0, move_orig.ids)], + } + ) + move = cls.env["stock.move"].create(move_vals) + picking = cls.env["stock.picking"].create(move._get_new_picking_values()) + move.picking_id = picking.id + return move + + @classmethod + def build_moves(cls): + """Build a chain of moves + + That looks like: + + PICK/001 ━► PACK/001 ┓ + ┃ + PICK/002 ┓ ┣━► OUT/001 + ┣━► PACK/002 ┛ + INT/001 ━► PICK/003 ┛ + """ + Chain = namedtuple("chain", "int1 pick1 pick2 pick3 pack1 pack2 out1") + + int1 = cls._create_move_with_picking(cls.int_type, cls.product, 2) + + pick1 = cls._create_move_with_picking(cls.pick_type, cls.product, 2) + pick2 = cls._create_move_with_picking(cls.pick_type, cls.product, 2) + pick3 = cls._create_move_with_picking( + cls.pick_type, cls.product, 2, move_orig=int1 + ) + + pack1 = cls._create_move_with_picking( + cls.pack_type, cls.product, 2, move_orig=pick1 + ) + + pack2 = cls._create_move_with_picking( + cls.pack_type, cls.product, 4, move_orig=pick2 + pick3 + ) + + out1 = cls._create_move_with_picking( + cls.out_type, cls.product, 6, move_orig=pack1 + pack2 + ) + + return Chain(int1, pick1, pick2, pick3, pack1, pack2, out1) + + def _enable_consolidate_priority(self, picking_types): + picking_types.consolidate_priority = True + picking_types.flush() + + def _test_query(self, starting_move, expected): + # we test only the result of the graph in this test, cancel/done move + # are not filtered out, as they are filtered later + query, params = starting_move._query_get_consolidate_moves() + self.env.cr.execute(query, params) + move_ids = [row[0] for row in self.env.cr.fetchall()] + self.assertEqual(set(move_ids), set(expected.ids)) + + def test_query_graph_out1(self): + self._enable_consolidate_priority(self.chain.out1.picking_type_id) + self._test_query( + self.chain.pick1, + self.chain.pick1 + | self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + ) + self._test_query( + self.chain.pick2, + self.chain.pick1 + | self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + ) + self._test_query( + self.chain.int1, + self.chain.pick1 + | self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + ) + + def test_query_graph_pack1(self): + self._enable_consolidate_priority(self.chain.pack1.picking_type_id) + self._test_query(self.chain.pick1, self.chain.pick1) + self._test_query( + self.chain.pick2, self.chain.int1 | self.chain.pick2 | self.chain.pick3 + ) + self._test_query( + self.chain.int1, self.chain.int1 | self.chain.pick2 | self.chain.pick3 + ) + + def test_query_graph_out1_pack1(self): + self._enable_consolidate_priority( + self.chain.out1.picking_type_id | self.chain.pack1.picking_type_id + ) + self._test_query( + self.chain.pick1, + self.chain.pick1 + | self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + ) + self._test_query( + self.chain.pick1, + self.chain.pick1 + | self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + ) + self._test_query( + self.chain.int1, + self.chain.pick1 + | self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + ) + + def _move_to_done(self, move): + self._update_qty_in_location( + move.location_id, move.product_id, move.product_uom_qty + ) + move.picking_id.action_assign() + for line in move.move_line_ids: + line.qty_done = line.product_uom_qty + move.picking_id.action_done() + self.assertEqual(move.state, "done") + + def assert_priority(self, changed_moves, unchanged_moves): + expected_priority = self.env["stock.move"]._consolidate_priority_value + for move in changed_moves: + self.assertEqual(move.priority, expected_priority) + for move in unchanged_moves: + self.assertEqual(move.priority, "1") + + def test_flow_pick1_done_out1_consolidate(self): + self._enable_consolidate_priority(self.chain.out1.picking_type_id) + self._move_to_done(self.chain.pick1) + self.assert_priority( + self.chain.pick2 + | self.chain.int1 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + # pick1 is unchanged as already done + self.chain.pick1 | self.chain.out1, + ) + + def test_flow_int1_done_out1_consolidate(self): + self._enable_consolidate_priority(self.chain.out1.picking_type_id) + self._move_to_done(self.chain.int1) + self.assert_priority( + self.chain.pick1 + | self.chain.pick2 + | self.chain.pick3 + | self.chain.pack1 + | self.chain.pack2, + # int1 is unchanged as already done + self.chain.int1 | self.chain.out1, + ) + + def test_flow_int1_done_pack_consolidate(self): + self._enable_consolidate_priority(self.chain.pack2.picking_type_id) + self._move_to_done(self.chain.int1) + self.assert_priority( + self.chain.pick2 | self.chain.pick3, + self.chain.pick1 | self.chain.pack1 + # pack2 is unchanged as the priority is raised *before* it + | self.chain.pack2 + # int1 is unchanged as already done + | self.chain.int1 | self.chain.out1, + ) + + def test_flow_pick2_done_pack_consolidate(self): + self._enable_consolidate_priority(self.chain.pack2.picking_type_id) + self._move_to_done(self.chain.pick2) + self.assert_priority( + self.chain.int1 | self.chain.pick3, + self.chain.pick1 | self.chain.pack1 + # pack2 is unchanged as the priority is raised *before* it + | self.chain.pack2 + # pick2 is unchanged as already done + | self.chain.pick2 | self.chain.out1, + ) From ab62cbc7dc28dd2c6c975800f94301c1707674b0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 8 Jul 2020 15:01:51 +0200 Subject: [PATCH 2/2] Raise priority only if the direct next move has consolidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the following graph: PICK/001 ━► PACK/001 ┓ ┃ PICK/002 ┓ ┣━► OUT/001 ┣━► PACK/002 ┛ INT/001 ━► PICK/003 ┛ If we have the consolidate flag on "PACK", we want to raise the priority only when we start to move goods in PACK/001 or PACK/002 (not INT/001), because this is in the packing zone that we are limited in space. Also, when for instance a move of PICK/002 or PICK/003 is set to done, *any* move (all products) that go to PACK/002 are concerned by the priority raise: we want to finish the transfer. --- .../models/stock_move.py | 102 ++--- .../tests/test_consolidation_priority.py | 364 +++++++++++------- 2 files changed, 283 insertions(+), 183 deletions(-) diff --git a/stock_picking_consolidation_priority/models/stock_move.py b/stock_picking_consolidation_priority/models/stock_move.py index b8ef3c9fc..bd75be529 100644 --- a/stock_picking_consolidation_priority/models/stock_move.py +++ b/stock_picking_consolidation_priority/models/stock_move.py @@ -21,80 +21,81 @@ class StockMove(models.Model): Consider this chain of moves:: - PICK/001 ━► PACK/001 ┓ - ┃ - PICK/002 ┓ ┣━► OUT/001 - ┣━► PACK/002 ┛ - PICK/003 ┛ + PICK/001 ━► PACK/001 ┓ + ┃ + PICK/002 ┓ ┣━► OUT/001 + ┣━► PACK/002 ┛ + INT/001 ━► PICK/003 ┛ If the flag "consolidate_priority" is set on the picking type of OUT, - when we set any of the PICK to done, the 3 PICK and the 2 PACK must - be returned to have their priority raised. + as soon as one of the move of PACK/001 or PACK/002 is done, all the + moves of the INT, 3 PICK, and the 2 PACK must be returned to have their + priority raised, as we want to consolidate everything asap in OUT/001. - If the flag is set on PACK and PICK/002 or PICK/003 is set to done, the - other PICK that leads to PACK/002 must be returned to have its priority - raised. But if only PICK/001 is set to done, nothing is returned. + If the flag is set on PACK and one move in PICK/001 is set to done, + other moves of PICK/001 are returned to finish PACK/001. + + If the flag is set on PACK and one move in PICK/002 is set to done, all + the moves of INT/001 and PICK/003 and the other moves of PICK/002 have + to be returned as they will help to consolidate PACK/002. + + + If the flag is set on PACK, when a move in INT/001 is set to done, + nothing happens, but when a move in PICK/003 is set to done, all the + moves of INT/001 and PICK/002 and the other moves of PICK/003 have to + be returned as they will help to consolidate PACK/002. If the flag is set on both PACK and OUT, the result is the same as the first case: all PICK and PACK are returned. """ query = """ WITH RECURSIVE - - -- Walk through all relations in stock_move_move_rel to find the - -- destination moves. For each move, join on stock_picking_type - -- to check if the flag "consolidate_priority" is set - destinations (id, consolidate_origins) AS ( - -- starting move - SELECT stock_move.id, - -- the moves here are done: their origin moves are - -- supposed to be done too, we don't need to raise their - -- priority - false as consolidate_origins - FROM stock_move - WHERE stock_move.id IN %s - UNION - -- recurse to find all the destinations - SELECT move_dest.id, - stock_picking_type.consolidate_priority as consolidate_origins - FROM stock_move move_dest - INNER JOIN stock_move_move_rel + -- For every destination move for which we have the flag set, walk back + -- through stock_move_move_rel to find all the origin moves + origins (id, picking_id, is_consolidation_dest) AS ( + -- Find the destination move of the current moves which have the flag + -- consolidate_priority on their picking type. From there, find all + -- the other moves of the consolidation transfer. + -- They are the starting point to search the origin moves. + -- In the final query, the rows with "consolidate_priority" equal + -- to true are excluded, because we want to raise the priority of + -- the moves *before* the picking type with the flag + SELECT consolidation_dest_moves.id, + stock_picking.id, + stock_picking_type.consolidate_priority as is_consolidation_dest + FROM stock_move_move_rel + INNER JOIN stock_move move_dest ON move_dest.id = stock_move_move_rel.move_dest_id INNER JOIN stock_picking ON stock_picking.id = move_dest.picking_id + -- select *all* the moves of the transfer with the consolidation flag, + -- origin moves will be searched for all of them in the recursive part + INNER JOIN stock_move consolidation_dest_moves + ON consolidation_dest_moves.picking_id = stock_picking.id INNER JOIN stock_picking_type ON stock_picking_type.id = stock_picking.picking_type_id - INNER JOIN destinations - ON destinations.id = stock_move_move_rel.move_orig_id - ), + WHERE stock_move_move_rel.move_orig_id IN %s + AND stock_picking_type.consolidate_priority = true - -- For every destination move for which we have the flag set, walk back - -- through stock_move_move_rel to find all the origin moves - origins (id, consolidate_origins) AS ( - -- for all the destinations which have to consolidate their origins, - -- it finds all the origin moves, in the final query, the rows with - -- "consolidate_origins" equal to true are excluded, because we want - -- to raise the priority of the moves *before* the picking type with - -- the flag - SELECT destinations.id, true - FROM destinations - WHERE consolidate_origins = true - -- We use union here to keep duplicate in case a move both has the flag - -- "consolidate_priority" on its picking type AND is the origin move - -- for another move with the flag. Anyway, the final query filters + -- We use union here to keep duplicate in case a move both has the + -- flag "consolidate_priority" on its picking type AND is the + -- origin move for another move with the flag (e.g, option + -- activated both on PACK and OUT). Anyway, the final query filters -- on the second part of the union. UNION ALL + -- recurse to find all the origin moves which have a destination that -- needs priority consolidation - SELECT move_orig.id, false + SELECT move_orig.id, + move_orig.picking_id, + false as is_consolidation_dest FROM stock_move move_orig INNER JOIN stock_move_move_rel ON move_orig.id = stock_move_move_rel.move_orig_id INNER JOIN origins ON origins.id = stock_move_move_rel.move_dest_id ) - SELECT DISTINCT id FROM origins - WHERE consolidate_origins = false + SELECT id FROM origins WHERE is_consolidation_dest = false """ return (query, (tuple(self.ids),)) @@ -105,6 +106,9 @@ class StockMove(models.Model): return {"priority": self._consolidate_priority_value} def _consolidate_priority(self): + self.flush(["move_dest_ids", "move_orig_ids", "picking_id"]) + self.env["stock.picking"].flush(["picking_type_id"]) + self.env["stock.picking.type"].flush(["consolidate_priority"]) query, params = self._query_get_consolidate_moves() self.env.cr.execute(query, params) move_ids = [row[0] for row in self.env.cr.fetchall()] diff --git a/stock_picking_consolidation_priority/tests/test_consolidation_priority.py b/stock_picking_consolidation_priority/tests/test_consolidation_priority.py index 20caacdda..83469d999 100644 --- a/stock_picking_consolidation_priority/tests/test_consolidation_priority.py +++ b/stock_picking_consolidation_priority/tests/test_consolidation_priority.py @@ -15,8 +15,11 @@ class TestConsolidationPriority(SavepointCase): cls.warehouse.write({"delivery_steps": "pick_pack_ship"}) cls.stock_shelf_location = cls.env.ref("stock.stock_location_components") cls.customers_location = cls.env.ref("stock.stock_location_customers") - cls.product = cls.env["product.product"].create( - {"name": "Product", "type": "product"} + cls.product_a = cls.env["product.product"].create( + {"name": "Product A", "type": "product"} + ) + cls.product_b = cls.env["product.product"].create( + {"name": "Product B", "type": "product"} ) cls.pick_type = cls.warehouse.pick_type_id @@ -34,9 +37,9 @@ class TestConsolidationPriority(SavepointCase): cls.env["stock.quant"]._update_available_quantity(product, location, quantity) @classmethod - def _create_move_with_picking(cls, picking_type, product, quantity, move_orig=None): + def _create_move(cls, name, picking_type, product, quantity, move_orig=None): move_vals = { - "name": product.name, + "name": name, "picking_type_id": picking_type.id, "product_id": product.id, "product_uom_qty": 2.0, @@ -56,14 +59,17 @@ class TestConsolidationPriority(SavepointCase): "move_orig_ids": [(6, 0, move_orig.ids)], } ) - move = cls.env["stock.move"].create(move_vals) - picking = cls.env["stock.picking"].create(move._get_new_picking_values()) - move.picking_id = picking.id - return move + return cls.env["stock.move"].create(move_vals) + + @classmethod + def _create_picking(cls, moves): + picking = cls.env["stock.picking"].create(moves._get_new_picking_values()) + moves.picking_id = picking.id + return picking @classmethod def build_moves(cls): - """Build a chain of moves + """Build a chain of moves and transfer That looks like: @@ -72,30 +78,81 @@ class TestConsolidationPriority(SavepointCase): PICK/002 ┓ ┣━► OUT/001 ┣━► PACK/002 ┛ INT/001 ━► PICK/003 ┛ + + Each contains 2 moves, one for product and one for product2 """ - Chain = namedtuple("chain", "int1 pick1 pick2 pick3 pack1 pack2 out1") - - int1 = cls._create_move_with_picking(cls.int_type, cls.product, 2) - - pick1 = cls._create_move_with_picking(cls.pick_type, cls.product, 2) - pick2 = cls._create_move_with_picking(cls.pick_type, cls.product, 2) - pick3 = cls._create_move_with_picking( - cls.pick_type, cls.product, 2, move_orig=int1 + Chain = namedtuple( + "chain", + "int1_a int1_b" + # slots for the moves (product a, product b) + " pick1_a pick1_b" + " pick2_a pick2_b" + " pick3_a pick3_b" + " pack1_a pack1_b" + " pack2_a pack2_b" + " out1_a out1_b", ) - pack1 = cls._create_move_with_picking( - cls.pack_type, cls.product, 2, move_orig=pick1 - ) + int1_a = cls._create_move("int1_a", cls.int_type, cls.product_a, 2) + int1_b = cls._create_move("int1_b", cls.int_type, cls.product_b, 2) + cls._create_picking(int1_a + int1_b) - pack2 = cls._create_move_with_picking( - cls.pack_type, cls.product, 4, move_orig=pick2 + pick3 - ) + pick1_a = cls._create_move("pick1_a", cls.pick_type, cls.product_a, 2) + pick1_b = cls._create_move("pick1_b", cls.pick_type, cls.product_b, 2) + cls._create_picking(pick1_a + pick1_b) - out1 = cls._create_move_with_picking( - cls.out_type, cls.product, 6, move_orig=pack1 + pack2 - ) + pick2_a = cls._create_move("pick2_a", cls.pick_type, cls.product_a, 2) + pick2_b = cls._create_move("pick2_b", cls.pick_type, cls.product_b, 2) + cls._create_picking(pick2_a + pick2_b) - return Chain(int1, pick1, pick2, pick3, pack1, pack2, out1) + pick3_a = cls._create_move( + "pick3_a", cls.pick_type, cls.product_a, 2, move_orig=int1_a + ) + pick3_b = cls._create_move( + "pick3_b", cls.pick_type, cls.product_b, 2, move_orig=int1_b + ) + cls._create_picking(pick3_a + pick3_b) + + pack1_a = cls._create_move( + "pack1_a", cls.pack_type, cls.product_a, 2, move_orig=pick1_a + ) + pack1_b = cls._create_move( + "pack1_b", cls.pack_type, cls.product_b, 2, move_orig=pick1_b + ) + cls._create_picking(pack1_a + pack1_b) + + pack2_a = cls._create_move( + "pack2_a", cls.pack_type, cls.product_a, 4, move_orig=pick2_a + pick3_a + ) + pack2_b = cls._create_move( + "pack2_b", cls.pack_type, cls.product_b, 4, move_orig=pick2_b + pick3_b + ) + cls._create_picking(pack2_a + pack2_b) + + out1_a = cls._create_move( + "out1_a", cls.out_type, cls.product_a, 6, move_orig=pack1_a + pack2_a + ) + out1_b = cls._create_move( + "out2_b", cls.out_type, cls.product_b, 6, move_orig=pack1_b + pack2_b + ) + cls._create_picking(out1_a + out1_b) + + return Chain( + int1_a, + int1_b, + pick1_a, + pick1_b, + pick2_a, + pick2_b, + pick3_a, + pick3_b, + pack1_a, + pack1_b, + pack2_a, + pack2_b, + out1_a, + out1_b, + ) def _enable_consolidate_priority(self, picking_types): picking_types.consolidate_priority = True @@ -107,79 +164,93 @@ class TestConsolidationPriority(SavepointCase): query, params = starting_move._query_get_consolidate_moves() self.env.cr.execute(query, params) move_ids = [row[0] for row in self.env.cr.fetchall()] - self.assertEqual(set(move_ids), set(expected.ids)) + moves = self.env["stock.move"].browse(move_ids) + self.assertEqual( + moves, + expected, + "Priorities are not correct.\n\nExpected:\n{}\n\nGot:\n{}".format( + "\n".join( + [ + "* {}: {}".format(move.id, move.name) + for move in expected.sorted() + ] + ), + "\n".join( + ["* {}: {}".format(move.id, move.name) for move in moves.sorted()] + ), + ), + ) def test_query_graph_out1(self): - self._enable_consolidate_priority(self.chain.out1.picking_type_id) - self._test_query( - self.chain.pick1, - self.chain.pick1 - | self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, - ) - self._test_query( - self.chain.pick2, - self.chain.pick1 - | self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, - ) - self._test_query( - self.chain.int1, - self.chain.pick1 - | self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, + # enable on OUT + self._enable_consolidate_priority(self.chain.out1_a.picking_type_id) + c = self.chain + # these ones do not directly bring goods to OUT + self._test_query(c.pick1_a, self.env["stock.move"]) + self._test_query(c.pick2_a, self.env["stock.move"]) + self._test_query(c.int1_a, self.env["stock.move"]) + self._test_query(c.pick3_a, self.env["stock.move"]) + all_before_out = ( + c.int1_a + + c.int1_b + + c.pick1_a + + c.pick1_b + + c.pick2_a + + c.pick2_b + + c.pick3_a + + c.pick3_b + + c.pack1_a + + c.pack1_b + + c.pack2_a + + c.pack2_b ) + # these ones do directly bring goods to OUT, they consolidate the moves + # before + self._test_query(c.pack1_a, all_before_out) + self._test_query(c.pack2_a, all_before_out) def test_query_graph_pack1(self): - self._enable_consolidate_priority(self.chain.pack1.picking_type_id) - self._test_query(self.chain.pick1, self.chain.pick1) - self._test_query( - self.chain.pick2, self.chain.int1 | self.chain.pick2 | self.chain.pick3 - ) - self._test_query( - self.chain.int1, self.chain.int1 | self.chain.pick2 | self.chain.pick3 + # enable on PACK + self._enable_consolidate_priority(self.chain.pack1_a.picking_type_id) + c = self.chain + # this one does not directly bring goods to PACK + self._test_query(c.int1_a, self.env["stock.move"]) + # these ones do directly bring goods to PACK, they consolidate the moves + # before + self._test_query(c.pick1_a, c.pick1_a + c.pick1_b) + all_before_pack2 = ( + c.int1_a + c.int1_b + c.pick2_a + c.pick2_b + c.pick3_a + c.pick3_b ) + self._test_query(c.pick2_a, all_before_pack2) + self._test_query(c.pick3_a, all_before_pack2) def test_query_graph_out1_pack1(self): + # enable on OUT and PACK self._enable_consolidate_priority( - self.chain.out1.picking_type_id | self.chain.pack1.picking_type_id + self.chain.out1_a.picking_type_id | self.chain.pack1_a.picking_type_id ) - self._test_query( - self.chain.pick1, - self.chain.pick1 - | self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, + c = self.chain + all_before_out = ( + c.int1_a + + c.int1_b + + c.pick1_a + + c.pick1_b + + c.pick2_a + + c.pick2_b + + c.pick3_a + + c.pick3_b + + c.pack1_a + + c.pack1_b + + c.pack2_a + + c.pack2_b ) - self._test_query( - self.chain.pick1, - self.chain.pick1 - | self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, - ) - self._test_query( - self.chain.int1, - self.chain.pick1 - | self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, + self._test_query(c.pack1_a, all_before_out) + self._test_query(c.pack2_a, all_before_out) + self._test_query(c.pick1_a, c.pick1_a + c.pick1_b) + all_before_pack2 = ( + c.int1_a + c.int1_b + c.pick2_a + c.pick2_b + c.pick3_a + c.pick3_b ) + self._test_query(c.pick2_a, all_before_pack2) def _move_to_done(self, move): self._update_qty_in_location( @@ -191,59 +262,84 @@ class TestConsolidationPriority(SavepointCase): move.picking_id.action_done() self.assertEqual(move.state, "done") + def all_chain_moves(self): + return self.env["stock.move"].union(*[c for c in self.chain]) + def assert_priority(self, changed_moves, unchanged_moves): expected_priority = self.env["stock.move"]._consolidate_priority_value - for move in changed_moves: - self.assertEqual(move.priority, expected_priority) - for move in unchanged_moves: - self.assertEqual(move.priority, "1") - - def test_flow_pick1_done_out1_consolidate(self): - self._enable_consolidate_priority(self.chain.out1.picking_type_id) - self._move_to_done(self.chain.pick1) - self.assert_priority( - self.chain.pick2 - | self.chain.int1 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, - # pick1 is unchanged as already done - self.chain.pick1 | self.chain.out1, + changed_ok = all(move.priority == expected_priority for move in changed_moves) + unchanged_ok = all(move.priority == "1" for move in unchanged_moves) + self.assertTrue( + changed_ok and unchanged_ok, + "Priorities are not correct.\n\nExpected:\n{}\n{}\n\nGot:\n{}".format( + "\n".join( + [ + "* {}: {}".format(move.name, expected_priority) + for move in changed_moves.sorted("id") + ] + ), + "\n".join( + [ + "* {}: {}".format(move.name, "1") + for move in unchanged_moves.sorted("id") + ] + ), + "\n".join( + [ + "* {}: {}".format(move.name, move.priority) + for move in self.all_chain_moves().sorted( + lambda m: (-int(m.priority), m.id) + ) + ] + ), + ), ) - def test_flow_int1_done_out1_consolidate(self): - self._enable_consolidate_priority(self.chain.out1.picking_type_id) - self._move_to_done(self.chain.int1) + def assert_default_priority(self): self.assert_priority( - self.chain.pick1 - | self.chain.pick2 - | self.chain.pick3 - | self.chain.pack1 - | self.chain.pack2, - # int1 is unchanged as already done - self.chain.int1 | self.chain.out1, + # nothing is changed yet + self.env["stock.move"], + # all moves are unchanged + self.all_chain_moves(), + ) + + def test_flow_pick1_done_out1_consolidate(self): + c = self.chain + # enable on OUT + self._enable_consolidate_priority(c.out1_a.picking_type_id) + self._move_to_done(self.chain.pick1_a) + self.assert_default_priority() + self._move_to_done(self.chain.pack1_a) + self.assert_priority( + c.int1_a + + c.int1_b + + c.pick1_b + + c.pick2_a + + c.pick2_b + + c.pick3_a + + c.pick3_b + + c.pack1_b + + c.pack2_a, + # pick1 and pack2 are unchanged as already done + c.pack1_a + c.pick1_a + c.out1_a + c.out1_b, ) def test_flow_int1_done_pack_consolidate(self): - self._enable_consolidate_priority(self.chain.pack2.picking_type_id) - self._move_to_done(self.chain.int1) + c = self.chain + # enable on PACK + self._enable_consolidate_priority(c.pack2_a.picking_type_id) + self._move_to_done(c.int1_a) + self.assert_default_priority() + self._move_to_done(c.pick3_a) self.assert_priority( - self.chain.pick2 | self.chain.pick3, - self.chain.pick1 | self.chain.pack1 - # pack2 is unchanged as the priority is raised *before* it - | self.chain.pack2 - # int1 is unchanged as already done - | self.chain.int1 | self.chain.out1, - ) - - def test_flow_pick2_done_pack_consolidate(self): - self._enable_consolidate_priority(self.chain.pack2.picking_type_id) - self._move_to_done(self.chain.pick2) - self.assert_priority( - self.chain.int1 | self.chain.pick3, - self.chain.pick1 | self.chain.pack1 - # pack2 is unchanged as the priority is raised *before* it - | self.chain.pack2 - # pick2 is unchanged as already done - | self.chain.pick2 | self.chain.out1, + c.int1_b + c.pick2_a + c.pick2_b + c.pick3_b, + # int1 and pick3 are unchanged as already done + c.int1_a + c.pick3_a + # only moves *before* packs are changed + + c.pack1_a + c.pack1_b + c.pack2_a + c.pack2_b + # pick1 is unchanged because they don't + # go to the same transfer + + c.pick1_a + c.pick1_b + # outgoing moves are late to the party, no impact on them + + c.out1_a + c.out1_b, )