Your Warehouse Is the Bottleneck You're Not Measuring
Most growing e-commerce and wholesale operations hit the same wall: order volume doubles, but warehouse throughput doesn't. Pickers walk the same aisles repeatedly, fulfilling one order at a time. Packing stations sit idle while pickers are in transit. Shipping cutoffs get missed because the last wave of orders wasn't prioritized. The result is overtime labor costs, late shipments, and customer churn that nobody traces back to warehouse operations.
Wave picking solves this by grouping orders into scheduled waves, assigning picks by zone, and releasing work to the floor in controlled batches. Instead of one picker walking the entire warehouse for one order, multiple pickers work their assigned zones simultaneously, and orders are consolidated at a packing station. The efficiency gain is dramatic—warehouses processing 500+ orders per day typically see a 30-50% throughput increase after implementing wave picking.
Odoo 19 ships with native wave picking support in the Inventory module. This guide covers: how wave picking differs from batch picking, how to create and schedule waves, how to configure zone-based picking, how to set up a pick-pack-ship workflow, and the performance metrics that prove it's working.
Batch Picking vs Wave Picking: Understanding the Core Difference in Odoo 19
Odoo supports both batch picking and wave picking, and the terminology causes confusion. They're related but solve different problems. Understanding the distinction is critical before you configure anything.
| Dimension | Batch Picking | Wave Picking |
|---|---|---|
| Grouping Logic | Manual or automatic grouping of transfers by picker capacity | Scheduled grouping based on time windows, carrier deadlines, or priority |
| Timing | On-demand — created when transfers are ready | Scheduled — released in timed waves aligned to shipping cutoffs |
| Zone Awareness | Optional — batch may span all zones | Core feature — waves are split by zone for parallel picking |
| Order Consolidation | Picker returns all items to a single cart | Items from multiple pickers converge at a consolidation/packing station |
| Best For | Small-to-medium warehouses, <200 orders/day | High-volume fulfillment, 500+ orders/day, multi-zone warehouses |
Wave picking is a superset of batch picking. Every wave contains one or more batches. The wave controls when work is released and how it's prioritized. The batch controls who picks what. In Odoo 19, you enable wave picking through the Batch Transfers feature in Inventory settings, then configure waves on top of batches.
Enabling Wave Picking in Odoo 19
Wave picking requires the Batch Transfers feature to be active. Navigate to the Inventory settings and enable it:
Settings > Inventory > Operations > Batch Transfers > Enable
Settings > Inventory > Operations > Wave Transfers > Enable
# Or via Python shell:
self.env['ir.config_parameter'].set_param(
'stock.picking_batch_wave', True
)Once enabled, you'll see a new "Waves" menu item under Inventory > Operations. The wave creation wizard becomes available on the Transfers list view, and batch transfers gain wave assignment fields.
Wave Creation and Scheduling: Aligning Picks to Shipping Cutoffs
The power of wave picking comes from controlled release. Instead of all pending transfers hitting the warehouse floor simultaneously, you release them in waves timed to carrier pickup schedules. A 10:00 AM FedEx pickup means your first wave releases at 7:00 AM, giving pickers three hours to complete picks, consolidate, pack, and stage for the carrier.
Creating a Wave from the Transfers List
The most common wave creation method is selecting transfers from the list view and using the "Add to Wave" action:
from odoo import fields
# Select transfers for the wave
transfers = self.env['stock.picking'].search([
('picking_type_id.code', '=', 'outgoing'),
('state', '=', 'assigned'),
('carrier_id.name', '=', 'FedEx Ground'),
('scheduled_date', '<=', fields.Datetime.now()),
])
# Create the wave
wave = self.env['stock.picking.batch'].create({
'name': f'WAVE-FedEx-{{fields.Date.today()}}',
'is_wave': True,
'picking_ids': [(6, 0, transfers.ids)],
'state': 'draft',
})
# Confirm and assign the wave
wave.action_confirm()Scheduling Waves with Automated Actions
For high-volume operations, manual wave creation doesn't scale. You need automated wave generation tied to your shipping schedule. Odoo's server actions and scheduled actions (cron jobs) handle this:
from odoo import api, fields, models
from datetime import timedelta
class StockWaveScheduler(models.Model):
_name = "stock.wave.scheduler"
_description = "Automated Wave Release Scheduler"
name = fields.Char(required=True)
carrier_id = fields.Many2one("delivery.carrier")
warehouse_id = fields.Many2one("stock.warehouse", required=True)
cutoff_hour = fields.Float(
string="Shipping Cutoff (24h)",
help="Hour of day for carrier pickup. "
"Waves release 3 hours before cutoff.",
)
max_picks_per_wave = fields.Integer(default=200)
def _cron_release_waves(self):
"""Called by ir.cron every 30 minutes."""
now = fields.Datetime.now()
for scheduler in self.search([]):
# Check if we're within the release window
cutoff = now.replace(
hour=int(scheduler.cutoff_hour),
minute=int((scheduler.cutoff_hour % 1) * 60),
)
release_time = cutoff - timedelta(hours=3)
if not (release_time <= now <= cutoff):
continue
# Find unassigned ready transfers
domain = [
('picking_type_id.warehouse_id', '=',
scheduler.warehouse_id.id),
('picking_type_id.code', '=', 'outgoing'),
('state', '=', 'assigned'),
('batch_id', '=', False),
]
if scheduler.carrier_id:
domain.append(
('carrier_id', '=', scheduler.carrier_id.id)
)
picks = self.env['stock.picking'].search(
domain,
limit=scheduler.max_picks_per_wave,
order='priority desc, scheduled_date asc',
)
if not picks:
continue
wave = self.env['stock.picking.batch'].create({
'is_wave': True,
'picking_ids': [(6, 0, picks.ids)],
})
wave.action_confirm() Notice the order='priority desc, scheduled_date asc' in the search. This ensures urgent orders (priority = 1) are included in the wave first, and within the same priority level, older orders are picked before newer ones. Without this, your FIFO discipline breaks and older orders keep getting pushed to later waves.
Zone-Based Picking: Parallel Execution Across Warehouse Zones
Zone-based picking is the multiplier that makes wave picking dramatically faster than single-picker workflows. Instead of one picker walking the entire warehouse, each picker is assigned a zone (e.g., Zone A: electronics, Zone B: apparel, Zone C: bulk items). When a wave is released, the system splits the work by zone, and all zone pickers work simultaneously.
Configuring Zones in Odoo 19
Zones in Odoo are represented as child locations under your main stock location. Each zone corresponds to a picking type with its own sequence and putaway rules:
# Create zone locations under the main warehouse
warehouse = self.env['stock.warehouse'].browse(1)
stock_location = warehouse.lot_stock_id
zones = {}
for zone_name in ['Zone-A-Electronics', 'Zone-B-Apparel', 'Zone-C-Bulk']:
zones[zone_name] = self.env['stock.location'].create({
'name': zone_name,
'location_id': stock_location.id,
'usage': 'internal',
'barcode': zone_name.upper(),
})
# Create zone-specific picking types
for zone_name, location in zones.items():
self.env['stock.picking.type'].create({
'name': f'Pick: {{zone_name}}',
'code': 'internal',
'warehouse_id': warehouse.id,
'default_location_src_id': location.id,
'default_location_dest_id': warehouse.wh_pack_stock_loc_id.id,
'sequence_code': f'PICK/{{zone_name[:1]}}',
'reservation_method': 'at_confirm',
})Splitting Waves by Zone
When a wave is created from multi-zone orders, the system needs to split the picking work so each zone picker gets only the items in their zone. This is handled by Odoo's multi-step routing—the pick operation is split by source location:
def split_wave_by_zone(self, wave):
"""Split a wave's picks into zone-specific batches."""
zone_batches = {}
for picking in wave.picking_ids:
for move in picking.move_ids:
zone = move.location_id
if zone not in zone_batches:
zone_batches[zone] = []
zone_batches[zone].append(move)
# Create a child batch per zone
for zone, moves in zone_batches.items():
pick_ids = list(set(m.picking_id.id for m in moves))
self.env['stock.picking.batch'].create({
'name': f'{{wave.name}}/{{zone.name}}',
'is_wave': False,
'picking_ids': [(6, 0, pick_ids)],
'user_id': zone.x_assigned_picker_id.id,
}) Zone-based picking only works if products are consistently stored in the correct zone. Configure putaway rules (stock.putaway.rule) so that receiving operations automatically route products to their designated zone. Without putaway rules, products end up in the default bin, and your zone assignments become meaningless. Audit your putaway coverage monthly—new products added without rules silently break the zone model.
Pick-Pack-Ship Workflow: The Three-Step Fulfillment Pipeline
Wave picking reaches its full potential when combined with Odoo's three-step delivery route: Pick → Pack → Ship. Each step is a separate transfer type with its own queue, its own workers, and its own validation. This separation is what allows zone pickers to hand off to packers, who hand off to shipping, with no idle time between stages.
Enabling Three-Step Delivery
Inventory > Configuration > Warehouses > [Your Warehouse]
> Outgoing Shipments: "Pack goods, send goods in output and then deliver (3 steps)"
# This creates three operation types automatically:
# 1. Pick (Stock > Packing Zone)
# 2. Pack (Packing Zone > Output)
# 3. Ship (Output > Customer)How Waves Flow Through Pick-Pack-Ship
Here's the complete workflow for a single wave:
| Stage | Operation Type | Who | What Happens |
|---|---|---|---|
| 1. Wave Release | — | Warehouse Manager / Cron | Wave is confirmed, pick operations become available for zone pickers |
| 2. Pick | WH/PICK | Zone Pickers | Each picker works their zone, scans items into a cart/tote, validates the pick transfer |
| 3. Consolidate | — | Packing Station | Items from multiple zones arrive at the packing station, grouped by sales order |
| 4. Pack | WH/PACK | Packers | Packer scans items, selects packaging, prints shipping label, validates pack transfer |
| 5. Ship | WH/OUT | Shipping Dock | Packed orders staged by carrier, loaded onto truck, ship transfer validated |
Tracking Wave Progress Programmatically
def get_wave_progress(self, wave_id):
"""Return wave completion metrics for dashboard."""
wave = self.env['stock.picking.batch'].browse(wave_id)
picks = wave.picking_ids
total = len(picks)
done = len(picks.filtered(lambda p: p.state == 'done'))
in_progress = len(picks.filtered(
lambda p: p.state == 'assigned'
))
waiting = len(picks.filtered(
lambda p: p.state in ('waiting', 'confirmed')
))
return {
'wave_name': wave.name,
'total_transfers': total,
'completed': done,
'in_progress': in_progress,
'waiting': waiting,
'completion_pct': round((done / total) * 100, 1)
if total else 0,
'estimated_finish': self._estimate_finish_time(
wave, done, total
),
}
def _estimate_finish_time(self, wave, done, total):
"""Estimate wave completion based on current velocity."""
if done == 0:
return None
elapsed = fields.Datetime.now() - wave.create_date
rate = done / elapsed.total_seconds() # picks per second
remaining = total - done
seconds_left = remaining / rate if rate > 0 else 0
return fields.Datetime.now() + timedelta(
seconds=seconds_left
)Wave picking without barcode scanning is like a kitchen without tickets—orders get lost. Enable Odoo's Barcode module and equip pickers with handheld scanners or mobile devices running the Odoo barcode interface. Every pick, pack, and ship step should be scan-validated. This eliminates mispicks (wrong item in wrong order), which are the #1 source of returns in high-volume fulfillment.
Performance Metrics and KPIs: Proving Wave Picking Is Working
Implementing wave picking without measuring its impact is guesswork. You need KPIs that connect warehouse operations to business outcomes. Here are the metrics that matter, and how to extract them from Odoo:
| KPI | Formula | Target | Odoo Data Source |
|---|---|---|---|
| Orders per Picker per Hour | Completed picks / picker hours | > 25 orders/hr | stock.picking timestamps + batch.user_id |
| Wave Completion Rate | Waves completed before cutoff / total waves | > 95% | stock.picking.batch state + scheduled time |
| Pick Accuracy | (Total picks - mispicks) / total picks | > 99.5% | Returns linked to stock.picking origin |
| Order Cycle Time | Ship confirm time - SO confirmation time | < 4 hours | sale.order date vs stock.picking done date |
| Zone Utilization | Active pick time / total shift time per zone | > 75% | Barcode scan timestamps per zone location |
Building a Wave Performance Query
-- Wave completion metrics for the last 30 days
SELECT
spb.name AS wave_name,
COUNT(sp.id) AS total_picks,
COUNT(sp.id) FILTER (
WHERE sp.state = 'done') AS completed,
ROUND(
COUNT(sp.id) FILTER (WHERE sp.state = 'done')::numeric
/ NULLIF(COUNT(sp.id), 0) * 100, 1
) AS completion_pct,
MIN(sp.date_done) AS first_pick_done,
MAX(sp.date_done) AS last_pick_done,
EXTRACT(EPOCH FROM (
MAX(sp.date_done) - MIN(sp.date_done)
)) / 60 AS wave_duration_minutes
FROM stock_picking_batch spb
JOIN stock_picking sp ON sp.batch_id = spb.id
WHERE spb.is_wave = TRUE
AND spb.create_date >= NOW() - INTERVAL '30 days'
GROUP BY spb.id, spb.name
ORDER BY spb.create_date DESC;Run a weekly warehouse ops review using these KPIs. The first two weeks after implementing wave picking will show disruption—pickers are learning new routes and the consolidation step adds unfamiliar complexity. By week three, throughput should exceed the pre-wave baseline. If it doesn't, the zone assignments are wrong or the wave sizes are too large. Reduce max picks per wave by 20% and reassess.
3 Wave Picking Mistakes That Kill Warehouse Throughput
Waves That Are Too Large: The 300-Pick Trap
The warehouse manager thinks bigger waves mean more efficiency. They create waves with 300-400 picks. The result: the wave takes so long to complete that it misses the shipping cutoff. Partially completed waves can't ship because some orders are still being picked while others are already packed. The packing station becomes a bottleneck as a flood of picks arrives simultaneously instead of flowing steadily.
Start with 50-100 picks per wave and increase gradually. Monitor wave completion time relative to your shipping cutoff window. The ideal wave size fills 60-70% of the available time before cutoff, leaving buffer for exceptions. Use the max_picks_per_wave parameter in the scheduler and adjust it weekly based on actual completion data.
Ignoring Reservation Conflicts Between Waves
Wave 1 reserves 50 units of SKU-A. Wave 2 needs 30 units of the same SKU, but they're all reserved by Wave 1. Wave 2 shows transfers in "Waiting" state, and the warehouse manager doesn't understand why. They manually unreserve Wave 1 to fix Wave 2, which breaks Wave 1's picks and forces a re-pick. This cascade of reservation conflicts is the #1 operational issue in wave picking implementations.
Set the reservation method on your pick operation type to manual or at_confirm and only confirm waves when inventory is verified. Before releasing a wave, run an availability check: wave.picking_ids.action_assign(). Any transfers that don't reach "Ready" state should be moved to the next wave, not force-reserved. Build a pre-release validation into your cron that excludes transfers with insufficient stock.
No Consolidation Step Between Pick and Pack
Zone pickers complete their picks and drop items at the packing station. But there's no consolidation process, so packers can't tell which items belong to which sales order. Items from Zone A and Zone B for the same order arrive at different times. Packers either wait (idle time) or start packing incomplete orders (mispacks and short ships). The result is worse than single-picker fulfillment because at least single pickers deliver complete orders.
Implement a consolidation buffer between pick and pack. Use Odoo's packing zone location as a staging area where items are sorted by sales order. Each order gets a tote or bin at the consolidation station. Zone pickers scan items into the correct tote. The packer only starts when all zones have completed their picks for that order—tracked by checking that all pick transfers linked to the same source sales order are in "done" state.
What Wave Picking Delivers to Your Bottom Line
Wave picking isn't a warehouse vanity project. It's an operational lever with measurable financial impact:
Parallel zone picking eliminates redundant aisle walks. Five pickers working five zones simultaneously process orders 3-5x faster than five pickers each walking the whole warehouse.
Fewer steps per pick, less idle time at packing stations, and elimination of overtime needed to meet shipping cutoffs. The same team handles 40% more volume.
Barcode-validated zone picking with consolidation checks virtually eliminates mispicks. Each return avoided saves $15-25 in reverse logistics cost plus the customer relationship.
For a warehouse processing 1,000 orders per day at an average fulfillment cost of $4.50 per order, a 40% efficiency gain saves $1,800 per day—$46,800 per month. That's before accounting for the revenue recovery from fewer late shipments and mispick-driven returns. The typical Odoo wave picking implementation pays for itself within the first month of operation.
Optimization Metadata
Complete guide to Odoo 19 wave picking. Configure batch and wave transfers, zone-based picking, pick-pack-ship workflows, and warehouse KPIs for high-volume fulfillment.
1. "Batch Picking vs Wave Picking: Understanding the Core Difference in Odoo 19"
2. "Wave Creation and Scheduling: Aligning Picks to Shipping Cutoffs"
3. "Zone-Based Picking: Parallel Execution Across Warehouse Zones"
4. "Pick-Pack-Ship Workflow: The Three-Step Fulfillment Pipeline"
5. "3 Wave Picking Mistakes That Kill Warehouse Throughput"