Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions stock_dynamic_routing/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
{
"name": "Stock Dynamic Routing",
"summary": "Dynamic routing of stock moves",
"author": "Camptocamp, Odoo Community Association (OCA)",
"author": "Camptocamp, BCIM, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/wms",
"maintainers": ["jbaudoux"],
"category": "Warehouse Management",
"version": "16.0.1.0.4",
"license": "AGPL-3",
"depends": ["stock", "stock_helper"],
"depends": [
# core
"stock",
],
"demo": [
"demo/stock_location_demo.xml",
"demo/stock_picking_type_demo.xml",
Expand Down
1 change: 1 addition & 0 deletions stock_dynamic_routing/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import stock_location
from . import stock_move
from . import stock_move_line
from . import stock_picking
from . import stock_routing
from . import stock_routing_rule
255 changes: 173 additions & 82 deletions stock_dynamic_routing/models/stock_move.py

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions stock_dynamic_routing/models/stock_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2026 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)

from odoo import models


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

def _log_message(self, record, move, template, vals):
if self.env.context.get("bypass_log_message"):
return
super()._log_message(record, move, template, vals)
return
6 changes: 6 additions & 0 deletions stock_dynamic_routing/models/stock_picking.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ class StockPicking(models.Model):
" canceled because it was left empty after a dynamic routing.",
)

def _check_entire_pack(self):
# This can be dropped in v16 as part of odoo standard
if self.env.context.get("bypass_entire_pack"):
return
return super()._check_entire_pack()

@api.depends("canceled_by_routing")
def _compute_state(self):
res = super()._compute_state()
Expand Down
4 changes: 2 additions & 2 deletions stock_dynamic_routing/models/stock_routing_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def _constrains_picking_type_location(self):

if record.method == "pull" and (
not record.location_src_id
or not record.location_src_id.is_sublocation_of(base_location)
or not record.location_src_id._child_of(base_location)
):
raise exceptions.ValidationError(
_(
Expand All @@ -89,7 +89,7 @@ def _constrains_picking_type_location(self):
)
elif record.method == "push" and (
not record.location_dest_id
or not record.location_dest_id.is_sublocation_of(base_location)
or not record.location_dest_id._child_of(base_location)
):

raise exceptions.ValidationError(
Expand Down
103 changes: 102 additions & 1 deletion stock_dynamic_routing/tests/test_routing_pull.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)

from odoo.tests import common
from odoo.tests import common, tagged


@tagged("post_install", "-at_install")
class TestRoutingPullCommon(common.TransactionCase):
@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -919,6 +920,106 @@ def test_change_dest_move_source_chain(self):

self.assertEqual(move_b.picking_id.picking_type_id, pick_type_routing_delivery)

def test_change_dest_move_source_chain_with_sublocation(self):
location_qa = self.env["stock.location"].create(
{"location_id": self.wh.wh_output_stock_loc_id.id, "name": "QA"}
)
location_qa_1 = self.env["stock.location"].create(
{"location_id": location_qa.id, "name": "QA: 1"}
)
# The setup we want is:
#
# * When the initial move line reserves in Highbay, the move is
# classified in picking type "Dynamic Routing" with locations
# Highbay -> Handover (a new move is inserted between Handover and
# Output)
# * When the next move source location is set to "Handover", we
# we want to classify the next move as "QA" with locations Handover
# -> Output/QA/1
# * When the last move source location is changed to "QA/1", it must be
# classified as "Delivery (after QA)" with locations Output/QA/1 ->
# Customer

pick_type_routing_qa = self.env["stock.picking.type"].create(
{
"name": "QA",
"code": "internal",
"sequence_code": "WH/QA",
"warehouse_id": self.wh.id,
"use_create_lots": False,
"use_existing_lots": True,
"default_location_src_id": self.location_handover.id,
"default_location_dest_id": location_qa_1.id,
}
)
self.env["stock.routing"].create(
{
"location_id": self.location_handover.id,
"picking_type_id": self.wh.pick_type_id.id,
"rule_ids": [
(
0,
0,
{"method": "pull", "picking_type_id": pick_type_routing_qa.id},
)
],
}
)

pick_type_routing_delivery = self.env["stock.picking.type"].create(
{
"name": "Delivery (after QA)",
"code": "outgoing",
"sequence_code": "OUT(R)",
"warehouse_id": self.wh.id,
"use_create_lots": False,
"use_existing_lots": True,
"default_location_src_id": location_qa.id,
"default_location_dest_id": self.customer_loc.id,
}
)
self.env["stock.routing"].create(
{
"location_id": location_qa.id,
"picking_type_id": self.wh.out_type_id.id,
"rule_ids": [
(
0,
0,
{
"method": "pull",
"picking_type_id": pick_type_routing_delivery.id,
},
)
],
}
)

pick_picking, customer_picking = self._create_pick_ship(
self.wh, [(self.product1, 10)]
)
move_a = pick_picking.move_ids
move_b = customer_picking.move_ids

self._update_product_qty_in_location(
self.location_hb_1_2, move_a.product_id, 100
)
pick_picking.action_assign()
move_middle = move_a.move_dest_ids
self.assertNotEqual(move_middle, move_b)

self.assert_src_highbay(move_a)
self.assert_dest_handover(move_a)
self.assert_src_handover(move_middle)
self.assertEqual(move_middle.location_dest_id, location_qa_1)
self.assertEqual(move_b.location_id, location_qa_1)
self.assert_dest_customer(move_b)

# routing has been applied
self.assertEqual(move_middle.picking_id.picking_type_id, pick_type_routing_qa)

self.assertEqual(move_b.picking_id.picking_type_id, pick_type_routing_delivery)

def test_mix_routing_reservation_same_location(self):
"""Test a picking with different types of routing

Expand Down
5 changes: 3 additions & 2 deletions stock_dynamic_routing/tests/test_routing_push.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)

from odoo.tests import common
from odoo.tests import common, tagged


@tagged("post_install", "-at_install")
class TestRoutingPush(common.TransactionCase):
@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -437,7 +438,7 @@ def test_several_move_ids(self):
moves = self.env["stock.move"].browse(
move.id for move in moves_with_routing_details
)
moves._apply_routing_rule_push(moves_with_routing_details)
moves._apply_routing_rule(moves_with_routing_details)
moves._action_assign()

# At this point, we should have this
Expand Down
3 changes: 2 additions & 1 deletion stock_dynamic_routing/tests/test_routing_rule.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)

from odoo.tests import common
from odoo.tests import common, tagged


@tagged("post_install", "-at_install")
class TestRoutingRule(common.TransactionCase):
@classmethod
def setUpClass(cls):
Expand Down
Loading