Running multiple warehouses adds complexity that WooCommerce wasn’t designed to handle natively. WooCommerce tracks a single stock quantity per product. Your ERP tracks stock across 3, 5, or 20 locations. Bridging this gap requires careful architecture decisions about how inventory is aggregated, how orders are routed, and how customers see availability.
The Multi-Warehouse Challenge
Your ERP shows:
Product: SKU-1234 "Blue Widget"
├── Warehouse A (New York): 50 units
├── Warehouse B (Los Angeles): 30 units
├── Warehouse C (Chicago): 0 units
└── Total Available: 80 units
What should WooCommerce show? Options:
- Total available (80) — simplest, but might promise stock that’s committed elsewhere
- Nearest warehouse — requires knowing customer location before they add to cart
- Fulfillment warehouse only — only show stock from the warehouse designated for online orders
- Available minus buffer — total minus a safety margin (e.g., 72 = 80 – 10%)
Architecture
ERP (multi-warehouse stock)
→ Aggregation Logic (middleware)
→ WooCommerce (single stock number per product)
WooCommerce (order placed)
→ Routing Logic (middleware)
→ ERP (assign to optimal warehouse)
The middleware handles the mismatch between ERP’s multi-location model and WooCommerce’s single-location model.
Inventory Aggregation Strategies
Strategy 1: Sum All Warehouses
function aggregateStock(erpItem) {
const totalStock = erpItem.warehouses.reduce(
(sum, wh) => sum + wh.available, 0
);
return {
sku: erpItem.sku,
stock_quantity: totalStock,
stock_status: totalStock > 0 ? 'instock' : 'outofstock'
};
}
Pros: Maximizes sellable inventory
Cons: May oversell if warehouse-specific commits aren’t tracked
Strategy 2: Primary Warehouse + Overflow
Designate one warehouse as the primary fulfillment center for online orders. Show its stock, but allow overflow from secondary warehouses:
function aggregateStock(erpItem, primaryWarehouseId) {
const primary = erpItem.warehouses.find(w => w.id === primaryWarehouseId);
const overflow = erpItem.warehouses
.filter(w => w.id !== primaryWarehouseId)
.reduce((sum, w) => sum + w.available, 0);
// Show primary stock + 50% of overflow (buffer for retail/wholesale)
const displayStock = (primary?.available || 0) + Math.floor(overflow * 0.5);
return {
sku: erpItem.sku,
stock_quantity: displayStock,
stock_status: displayStock > 0 ? 'instock' : 'outofstock'
};
}
Strategy 3: Regional Display (Advanced)
Show different stock levels based on the customer’s detected region:
async function getRegionalStock(erpItem, customerRegion) {
const regionWarehouseMap = {
'east': ['WH-NYC', 'WH-ATL'],
'west': ['WH-LAX', 'WH-SEA'],
'central': ['WH-CHI', 'WH-DAL']
};
const relevantWarehouses = regionWarehouseMap[customerRegion] || Object.values(regionWarehouseMap).flat();
const regionalStock = erpItem.warehouses
.filter(w => relevantWarehouses.includes(w.id))
.reduce((sum, w) => sum + w.available, 0);
return { stock_quantity: regionalStock };
}
This requires dynamic stock display on the frontend — typically via an AJAX call after detecting the customer’s location.
Order Routing
When an order comes in, your middleware must decide which warehouse fulfills it:
function routeOrder(order, warehouseStock) {
const shippingState = order.shipping.state;
const items = order.line_items;
// 1. Find warehouses that can fulfill the entire order
const candidates = warehouseStock.filter(wh =>
items.every(item =>
wh.inventory[item.sku] >= item.quantity
)
);
if (candidates.length === 0) {
// No single warehouse can fulfill — need split shipment
return splitShipment(order, warehouseStock);
}
// 2. Score candidates
const scored = candidates.map(wh => ({
warehouse: wh,
score: calculateScore(wh, shippingState, items)
}));
// 3. Select best warehouse
return scored.sort((a, b) => b.score - a.score)[0].warehouse;
}
function calculateScore(warehouse, customerState, items) {
let score = 0;
// Proximity (shipping zone distance)
const distance = getDistance(warehouse.state, customerState);
score += (1000 - distance) * 2; // Closer = higher score
// Stock depth (prefer warehouses with more stock)
const minStockRatio = Math.min(
...items.map(item => warehouse.inventory[item.sku] / item.quantity)
);
score += minStockRatio * 100;
// Workload (prefer less busy warehouses)
score -= warehouse.pendingOrders * 5;
return score;
}
Split Shipments
When no single warehouse can fulfill the entire order:
function splitShipment(order, warehouses) {
const shipments = [];
const remaining = [...order.line_items];
// Greedy algorithm: assign items to nearest warehouse with stock
while (remaining.length > 0) {
const item = remaining[0];
const warehouse = warehouses
.filter(wh => wh.inventory[item.sku] >= item.quantity)
.sort((a, b) =>
getDistance(a.state, order.shipping.state) -
getDistance(b.state, order.shipping.state)
)[0];
if (!warehouse) {
throw new Error(Cannot fulfill ${item.sku}: insufficient stock across all warehouses);
}
// Find or create shipment for this warehouse
let shipment = shipments.find(s => s.warehouseId === warehouse.id);
if (!shipment) {
shipment = { warehouseId: warehouse.id, items: [] };
shipments.push(shipment);
}
shipment.items.push(item);
warehouse.inventory[item.sku] -= item.quantity;
remaining.shift();
}
return shipments;
}
Customer communication for split shipments:
- Notify customer that their order will arrive in multiple packages
- Provide separate tracking numbers per shipment
- Don’t charge extra shipping (absorb the cost)
WooCommerce Implementation
Plugin Approach
Use a multi-warehouse plugin (like ATUM or WooCommerce Multi-Warehouse) that adds per-warehouse stock tracking to WooCommerce. Your ERP sync updates each warehouse’s stock level.
Custom Meta Approach
Store warehouse-level data in product meta:
// Store per-warehouse stock in product meta
update_post_meta($product_id, '_warehouse_stock', json_encode([
'WH-NYC' => 50,
'WH-LAX' => 30,
'WH-CHI' => 0
]));
// Display stock is the aggregated number
update_post_meta($product_id, '_stock', 80);
update_post_meta($product_id, '_stock_status', 'instock');
Monitoring Multi-Warehouse Operations
| Metric | What to Track |
|---|---|
| Stock accuracy | ERP stock vs WooCommerce displayed stock |
| Routing efficiency | % of orders fulfilled from nearest warehouse |
| Split shipment rate | % of orders requiring split shipments (target: < 5%) |
| Fulfillment time by warehouse | Identify slow warehouses |
| Stock-out events | When aggregated stock hits zero |
Conclusion
Multi-warehouse WooCommerce is fundamentally an ERP problem solved through middleware. WooCommerce displays a simplified view; your ERP manages the complexity. The key decisions — aggregation strategy, routing logic, and split shipment handling — depend on your specific logistics setup. Start with the simplest aggregation (sum or primary warehouse), implement basic routing, and add complexity only when the data shows you need it. Over-engineering warehouse logic before you have the order volume to justify it wastes development time.
Last modified: April 3, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe