Raise priority only if the direct next move has consolidation

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.
This commit is contained in:
Guewen Baconnier
2020-07-08 15:01:51 +02:00
parent d6b34b968f
commit ab62cbc7dc
2 changed files with 283 additions and 183 deletions

View File

@@ -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()]

View File

@@ -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,
)