Merge PR #941 into 13.0

Signed-off-by simahawk
This commit is contained in:
OCA-git-bot
2020-09-02 13:04:52 +00:00
13 changed files with 531 additions and 0 deletions

View File

@@ -0,0 +1 @@
../../../../stock_picking_consolidation_priority

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -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",
}

View File

@@ -0,0 +1,2 @@
from . import stock_picking_type
from . import stock_move

View File

@@ -0,0 +1,122 @@
# 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 ┛
INT/001 ━► PICK/003 ┛
If the flag "consolidate_priority" is set on the picking type of OUT,
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 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
-- 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
WHERE stock_move_move_rel.move_orig_id IN %s
AND stock_picking_type.consolidate_priority = 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 (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,
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 id FROM origins WHERE is_consolidation_dest = 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):
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()]
if not move_ids:
return
moves = self.search(
expression.AND(
[[("id", "in", move_ids)], self._consolidate_priority_domain()]
)
)
moves.write(self._consolidate_priority_values())

View File

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

View File

@@ -0,0 +1 @@
Update the flag "Raise priority when partially available" on Operation Types.

View File

@@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>

View File

@@ -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.

View File

@@ -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.

View File

@@ -0,0 +1 @@
from . import test_consolidation_priority

View File

@@ -0,0 +1,345 @@
# 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_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
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(cls, name, picking_type, product, quantity, move_orig=None):
move_vals = {
"name": 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)],
}
)
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 and transfer
That looks like:
PICK/001 ━► PACK/001 ┓
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_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",
)
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)
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)
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)
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
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()]
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):
# 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):
# 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_a.picking_type_id | self.chain.pack1_a.picking_type_id
)
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(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(
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 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
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 assert_default_priority(self):
self.assert_priority(
# 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):
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(
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,
)