Your Warehouse Isn't Slow Because of People—It's Slow Because of Routing
Most Odoo implementations get warehousing wrong in the same way: everything runs through a single warehouse with one-step receipts and one-step deliveries. It works for the first six months. Then the business adds a second warehouse, or starts dropshipping, or needs quality inspection before putaway—and the entire inventory flow collapses into manual transfers and spreadsheet workarounds.
The root cause is almost always misconfigured routing rules. Odoo 19's inventory module ships with a powerful routing engine built on push rules, pull rules, and procurement groups. But the terminology is confusing, the UI hides critical settings behind "Storage Locations" and "Multi-Step Routes" checkboxes, and the documentation assumes you already understand supply chain theory.
This guide explains every layer of Odoo 19 warehouse routing from first principles: how routes and rules work internally, when to use push vs. pull, how Make-to-Order differs from Make-to-Stock, how to set up inter-warehouse replenishment, and how to configure multi-step reception and delivery flows.
Understanding Routes and Rules: The Core of Odoo Warehouse Routing
Before configuring anything, you need to understand the data model. In Odoo 19, all inventory movement automation flows through three objects: Routes, Rules, and Operation Types. Misunderstanding how these connect is the #1 cause of warehouse routing bugs.
The Routing Hierarchy
| Object | Model | Purpose | Example |
|---|---|---|---|
| Route | stock.route | A named container that groups one or more rules | "Receive in 2 steps (Input + Stock)" |
| Rule | stock.rule | A single instruction: move product from location A to location B using action X | "When product arrives at Input, push it to Stock via internal transfer" |
| Operation Type | stock.picking.type | Defines the type of transfer (receipt, delivery, internal) | "WH: Internal Transfers" |
A Route is applied to a product, a product category, a warehouse, or a sales order line. When inventory needs to move, Odoo searches for applicable routes, finds matching rules, and creates stock transfers (pickings) accordingly. The route itself does nothing—the rules inside it do all the work.
Where Routes Can Be Applied
| Scope | Field Location | Priority | Use Case |
|---|---|---|---|
| Sales Order Line | sale.order.line.route_id | Highest | Override routing for a single order (dropship this line) |
| Product | product.template.route_ids | High | This product always uses MTO or a specific warehouse route |
| Product Category | product.category.route_ids | Medium | All products in "Raw Materials" use a 2-step receipt with QC |
| Warehouse | stock.warehouse.route_ids | Default | The warehouse's default reception and delivery routes |
When multiple routes match, Odoo resolves them from most specific to least specific: SO line → Product → Category → Warehouse. If a product has an explicit route, the warehouse default is ignored for that product. This is how you can run most products on MTS while a handful use MTO—without changing the warehouse configuration.
Enabling Multi-Step Routing in Settings
# Navigate to: Inventory > Configuration > Settings
# Under "Warehouse" section, enable:
[x] Storage Locations # Enables location-level tracking
[x] Multi-Step Routes # Unlocks push/pull rule configuration
# These two checkboxes unlock the entire routing engine.
# Without them, you only get 1-step receipt and 1-step delivery.Push Rules: Automatic Transfers Triggered by Incoming Stock
Push rules are the simplest routing concept in Odoo: when product arrives at location A, automatically create a transfer to move it to location B. No demand signal is needed. The mere presence of stock at the source location triggers the next move.
How Push Rules Work Internally
When a stock move is confirmed and its destination matches a push rule's source location, Odoo automatically creates a chained move from the push rule's source to its destination. The chain can be multiple steps deep: Input → Quality Control → Stock, for example, uses two push rules.
# A push rule is a stock.rule with action = 'push'
# Example: automatically move goods from Input to Stock
rule = env['stock.rule'].create({
'name': 'Input → Stock (Auto Push)',
'action': 'push',
'route_id': receipt_2step_route.id,
'location_src_id': input_location.id, # Source: WH/Input
'location_dest_id': stock_location.id, # Destination: WH/Stock
'picking_type_id': internal_transfer.id, # Op Type: Internal Transfer
'auto': 'manual', # 'manual' = create picking, 'transparent' = no picking
'delay': 0, # Days to wait before creating the move
})The auto Field: Picking vs. Transparent
| Value | Behavior | Use Case |
|---|---|---|
manual | Creates a new picking (transfer document) that a warehouse worker must validate | Input → Stock where someone physically moves the goods |
transparent | No picking created; the move happens silently in the background | Virtual location transitions (e.g., partner location → input zone) |
Real-World Example: 2-Step Receipt with Quality Check
<odoo>
<!-- Route: Receive in 2 Steps (Input + Quality) -->
<record id="route_receipt_2step_qc" model="stock.route">
<field name="name">Receipt: Input → QC → Stock</field>
<field name="warehouse_selectable">True</field>
<field name="sequence">10</field>
</record>
<!-- Push Rule 1: Input → Quality Control -->
<record id="rule_push_input_to_qc" model="stock.rule">
<field name="name">Input → Quality Control</field>
<field name="action">push</field>
<field name="route_id" ref="route_receipt_2step_qc"/>
<field name="location_src_id" ref="stock.stock_location_company"/>
<field name="location_dest_id" ref="quality_control_location"/>
<field name="picking_type_id" ref="stock.picking_type_internal"/>
<field name="auto">manual</field>
</record>
<!-- Push Rule 2: Quality Control → Stock -->
<record id="rule_push_qc_to_stock" model="stock.rule">
<field name="name">Quality Control → Stock</field>
<field name="action">push</field>
<field name="route_id" ref="route_receipt_2step_qc"/>
<field name="location_src_id" ref="quality_control_location"/>
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="picking_type_id" ref="stock.picking_type_internal"/>
<field name="auto">manual</field>
</record>
</odoo>Push rules are ideal for supply-driven flows: goods arrive and need to be routed to the right place regardless of demand. Quality inspection, cross-docking zones, hazmat segregation, and cold chain staging areas are all push-rule territory. If the trigger is "product showed up here," use a push rule.
Pull Rules: Demand-Driven Procurement and Replenishment
Pull rules work in the opposite direction from push rules: demand at location B triggers a procurement action to bring product from location A. A sales order confirms, Odoo sees demand at the customer location, and pull rules cascade backward through the supply chain to fulfill it.
How Pull Rules Cascade
When a need (procurement) is created at a destination location, Odoo searches for a pull rule whose destination matches. The rule creates a stock move and, if the source location also has a pull rule, a new procurement cascades backward. This chain continues until it reaches a source with no further pull rules (typically a vendor location or a manufacturing location).
# Customer confirms Sales Order
# Demand: 50 units at Customer Location
Step 1 (Pull Rule): Customer Location <-- Output Zone
→ Creates: Delivery Order (WH/OUT)
Step 2 (Pull Rule): Output Zone <-- Packing Zone
→ Creates: Packing Transfer (WH/PACK)
Step 3 (Pull Rule): Packing Zone <-- Stock
→ Creates: Pick Transfer (WH/PICK)
# All three transfers are created simultaneously
# when the SO is confirmed. Workers process them
# in reverse order: Pick → Pack → Ship.Pull Rule Actions
| Action | Code | What Happens | Use Case |
|---|---|---|---|
| Pull From | pull | Creates a stock move to bring goods from the source location | Standard delivery, internal transfers |
| Pull & Push | pull_push | Combines pull behavior with an automatic push at the destination | Cross-dock scenarios |
| Buy | buy | Creates a purchase order (RFQ) instead of a stock move | MTO purchases, automated replenishment |
| Manufacture | manufacture | Creates a manufacturing order | MTO production, kit assembly |
# Pull rule with 'buy' action: demand triggers a PO
rule = env['stock.rule'].create({
'name': 'Buy from Vendor when Needed',
'action': 'buy',
'route_id': mto_route.id,
'location_dest_id': stock_location.id,
'picking_type_id': receipt_type.id,
'procure_method': 'make_to_order', # Key field for MTO
'group_propagation_policy': 'propagate',
'delay': 7, # Expected vendor lead time in days
})Push = supply-driven (goods arrive, route them automatically). Pull = demand-driven (someone needs goods, go get them). Most warehouse flows use a combination: pull rules cascade from the sales order backward to create all the transfers, and push rules handle internal routing after goods arrive at intermediate locations. Understanding this duality is the key to designing any Odoo warehouse flow.
Make-to-Order vs. Make-to-Stock: Choosing the Right Procurement Strategy
MTO and MTS are not routes—they're procurement methods defined on pull rules. This distinction matters because most Odoo users confuse the MTO route with the MTO procurement method, leading to configurations that either over-order or under-deliver.
The Two Procurement Methods
| Method | Field Value | Behavior | Inventory Impact |
|---|---|---|---|
| Make to Stock (MTS) | make_to_stock | Fulfills demand from existing stock. If stock is insufficient, the delivery waits. | Requires safety stock and reorder rules to avoid stockouts |
| Make to Order (MTO) | make_to_order | Every demand triggers a new procurement upstream (purchase, manufacture, or transfer) | Zero inventory carrying cost but longer lead times |
Configuring MTO on a Product
# 1. Enable the MTO route on the product
# Product form → Inventory tab → Routes
# Check: "Replenish on Order (MTO)"
# 2. Ensure the product has a vendor or a BoM
# MTO needs a source. Without a vendor (for 'buy' rules)
# or a Bill of Materials (for 'manufacture' rules),
# the procurement will fail with:
# "No rule has been found to replenish..."
# 3. What happens when a SO confirms:
# - SO line creates demand at Customer Location
# - Delivery pull rule: Customer <-- WH/Stock (procure_method='make_to_order')
# - Because procure_method='make_to_order', Odoo does NOT check stock
# - Instead, it cascades to the next rule: Buy or Manufacture
# - Result: PO or MO is auto-created, linked to the SOMTO + MTS Hybrid: The Best of Both Worlds
Odoo 19 supports a hybrid approach using reorder rules with MTO. The product uses MTS by default (ships from stock), but a reorder rule with "Make to Order" checked triggers replenishment only when stock drops below the minimum. This gives you fast delivery from stock with demand-driven replenishment:
# Create a reorder rule that triggers on-demand procurement
reorder = env['stock.warehouse.orderpoint'].create({
'product_id': product.id,
'warehouse_id': warehouse.id,
'location_id': stock_location.id,
'product_min_qty': 10, # Reorder when stock falls below 10
'product_max_qty': 50, # Order up to 50 units
'qty_multiple': 5, # Order in multiples of 5
'route_id': buy_route.id, # Use the "Buy" route for procurement
'trigger': 'auto', # Run scheduler automatically
})
# With this setup:
# - Sales orders ship from stock immediately (MTS behavior)
# - When stock hits 10, a PO is auto-created for 40 units
# - No manual intervention neededUse pure MTO only for high-value, low-volume, or custom-configured products where carrying inventory is expensive or impossible. Examples: custom machinery, made-to-spec components, or products with short shelf life. For everything else, MTS with reorder rules gives faster delivery and smoother operations.
Inter-Warehouse Replenishment: Automated Stock Transfers Between Locations
When you operate multiple warehouses, you need a way to move stock between them automatically. Odoo 19 provides inter-warehouse replenishment routes that let a secondary warehouse pull stock from a primary warehouse using the same rule engine that handles single-warehouse flows.
Setting Up a Resupply Route
# 1. Go to: Inventory → Configuration → Warehouses
# 2. Open the SECONDARY warehouse (the one that needs stock)
# 3. Under "Resupply From" → check the PRIMARY warehouse
# What Odoo creates automatically:
# - A new route: "Secondary: Supply Product from Primary"
# - A pull rule: Secondary/Stock <-- Primary/Stock
# with action='pull' and picking type = inter-warehouse transit
# 4. Apply this route to products or categories:
# Product → Inventory tab → Routes
# Check: "Secondary: Supply Product from Primary"
# 5. Create reorder rules on the secondary warehouse
# to trigger automatic replenishment when stock is lowThe Transit Location
Inter-warehouse transfers flow through a transit location (stock.location with usage='transit'). This is critical for accounting: goods in transit are neither in the source warehouse nor the destination. The transfer creates two pickings:
# Transfer: Primary WH → Secondary WH (50 units of Product A)
Picking 1 (Delivery from Primary):
Source: Primary/Stock
Destination: Inter-Company Transit
Op Type: Primary: Delivery Orders
Picking 2 (Receipt at Secondary):
Source: Inter-Company Transit
Destination: Secondary/Stock
Op Type: Secondary: Receipts
# Both pickings are linked via the same procurement group.
# Picking 2 becomes "Waiting" until Picking 1 is validated.
# This ensures accurate stock valuation during transit.Automating with Reorder Rules
# Reorder rule on the secondary warehouse
# Triggers when stock falls below minimum
orderpoint = env['stock.warehouse.orderpoint'].create({
'product_id': product.id,
'warehouse_id': secondary_wh.id,
'location_id': secondary_wh.lot_stock_id.id,
'product_min_qty': 20,
'product_max_qty': 100,
'qty_multiple': 10,
'route_id': resupply_route.id, # The auto-created resupply route
'trigger': 'auto',
})
# When the scheduler runs (or on SO confirmation if trigger='manual'):
# 1. Stock at Secondary <= 20 → procurement created
# 2. Resupply route finds the pull rule: Secondary <-- Primary
# 3. Two pickings created: Primary delivery + Secondary receipt
# 4. Warehouse workers at both sites see their tasksIf your warehouses belong to different companies, you need inter-company rules (which generate purchase orders between entities) instead of simple inter-warehouse routes. Inter-warehouse resupply only works within a single company. For multi-company setups, configure the "Inter-Company Trade" setting and create vendor/customer relationships between your companies.
Multi-Step Reception and Delivery: Configuring Complex Warehouse Flows
Odoo 19 warehouses support 1, 2, or 3-step flows for both incoming and outgoing goods. The number of steps is configured per warehouse and determines which routes and rules Odoo auto-generates.
Reception Steps Compared
| Steps | Flow | Locations Used | Best For |
|---|---|---|---|
| 1-Step | Vendor → Stock | WH/Stock | Small warehouses, low volume, no QC needed |
| 2-Step | Vendor → Input → Stock | WH/Input, WH/Stock | Receiving dock staging, batch putaway |
| 3-Step | Vendor → Input → QC → Stock | WH/Input, WH/Quality Control, WH/Stock | Regulated industries, high-value goods |
Delivery Steps Compared
| Steps | Flow | Locations Used | Best For |
|---|---|---|---|
| 1-Step | Stock → Customer | WH/Stock | Direct shipping, small operations |
| 2-Step | Stock → Output → Customer | WH/Stock, WH/Output | Pick and ship, staging area needed |
| 3-Step | Stock → Pack → Output → Customer | WH/Stock, WH/Packing Zone, WH/Output | E-commerce, B2B with specific packing requirements |
Changing Steps on an Existing Warehouse
# Change delivery from 1-step to 3-step (Pick + Pack + Ship)
warehouse = env['stock.warehouse'].browse(1)
warehouse.write({
'delivery_steps': 'pick_pack_ship',
# Options: 'ship_only', 'pick_ship', 'pick_pack_ship'
})
# Change reception from 1-step to 2-step (Input + Stock)
warehouse.write({
'reception_steps': 'two_steps',
# Options: 'one_step', 'two_steps', 'three_steps'
})
# Odoo automatically:
# 1. Creates the new locations (WH/Packing Zone, WH/Output)
# 2. Creates new operation types (WH: Pick, WH: Pack)
# 3. Creates the route with chained pull rules
# 4. Archives the old 1-step route
# WARNING: Changing steps while transfers are in progress
# does NOT retroactively update existing pickings.
# Complete or cancel all open transfers first.Mixing Steps Across Warehouses
Each warehouse can have its own step configuration. Your main distribution center might use 3-step delivery (Pick + Pack + Ship) while a satellite warehouse uses 1-step. The routing engine handles this transparently—sales orders routed to different warehouses will create the appropriate number of transfers based on each warehouse's configuration.
Every additional step creates an additional stock.picking record and associated stock.move records. For high-volume warehouses processing 1,000+ orders per day, 3-step delivery means 3,000+ pickings daily. Make sure your scheduler cron (ir.cron for stock.warehouse.orderpoint) and your database can handle the load. Index stock_move.state and stock_picking.state if you haven't already.
3 Multi-Warehouse Routing Mistakes That Cause Inventory Chaos
Enabling MTO Without a Fallback Procurement Source
You check the "Replenish on Order (MTO)" route on a product and confirm a sales order. Instead of a purchase order or manufacturing order appearing, you get a red error: "No rule has been found to replenish product X in WH/Stock." The sales order is stuck, the customer is waiting, and your operations team has no idea what to do.
MTO is not a complete route—it's a modifier on the procurement method. It tells Odoo "don't fulfill from stock, cascade upstream." But "upstream" needs to exist. The product must have either a vendor with a vendor pricelist (for the Buy rule) or a Bill of Materials (for the Manufacture rule). Before enabling MTO on any product, verify the supply chain is configured end-to-end by running Inventory → Operations → Replenishment and checking for errors.
Conflicting Routes on Products and Warehouses
A product has both the "Buy" route and the warehouse's default "Receive in 1 step" route. A reorder rule triggers replenishment. Odoo creates two separate procurements—one from each route—and you end up with duplicate purchase orders. Or worse: the rules conflict, and Odoo picks the wrong one based on sequence numbers you never configured.
Audit every product's route assignments with a domain search: env['product.template'].search([('route_ids', '!=', False)]). Products should have at most one procurement route (Buy, Manufacture, or a custom resupply route). The warehouse routes (reception/delivery steps) are separate and don't conflict. When in doubt, remove product-level route assignments and let the warehouse defaults handle routing. Only add product-level routes for exceptions (MTO products, dropship items).
Changing Warehouse Steps with Open Transfers in Progress
You switch your warehouse from 1-step to 3-step delivery while 200 delivery orders are in "Ready" state. The existing pickings don't update—they still reference the old route and locations. New orders use the 3-step flow. Now you have two parallel workflows running in the same warehouse, workers are confused about which transfers to process, and some orders ship directly from Stock while others go through Pick → Pack → Ship.
Never change warehouse steps during business hours with open transfers. Schedule the change during a maintenance window. Before switching: validate or cancel all open pickings, run the scheduler to clear pending procurements, and take a database backup. After switching: verify the new locations exist, test with a single order end-to-end, and only then resume normal operations. Document the cutover time so your accounting team knows which transfers used which flow.
What Proper Multi-Warehouse Routing Saves Your Business
Routing isn't a technical exercise. It's a direct lever on fulfillment speed, inventory cost, and order accuracy:
Multi-step routing with automated transfers eliminates manual handoffs. Workers see exactly what to pick, pack, and ship—no guessing, no spreadsheets, no radio calls to the dock.
MTO for slow-movers and inter-warehouse replenishment rules mean you stock the right products in the right warehouse. No more overstocking satellite locations "just in case."
Automated push and pull rules replace manual transfer creation. When the system decides where goods go, human routing errors drop to near zero.
For a company operating 3 warehouses with 500 SKUs, properly configured routing typically eliminates 2-3 full-time-equivalent hours per day of manual transfer creation and reduces mis-ships by 80%. Over a year, that's 700+ labor hours saved and thousands of dollars in shipping corrections avoided.
Optimization Metadata
Complete guide to Odoo 19 multi-warehouse routing. Learn push rules, pull rules, MTO vs MTS strategies, inter-warehouse replenishment, and multi-step reception/delivery configuration.
1. "Understanding Routes and Rules: The Core of Odoo Warehouse Routing"
2. "Push Rules: Automatic Transfers Triggered by Incoming Stock"
3. "Pull Rules: Demand-Driven Procurement and Replenishment"
4. "Make-to-Order vs. Make-to-Stock: Choosing the Right Procurement Strategy"
5. "Inter-Warehouse Replenishment: Automated Stock Transfers Between Locations"
6. "Multi-Step Reception and Delivery: Configuring Complex Warehouse Flows"