🇹🇷 Türkçe: Bu yazının Türkçesini oku →

Running multiple warehouses adds complexity that WooCommerce wasn’t designed to handle natively, and I want to be direct about that from the first line: WooCommerce tracks a single stock quantity per product, and no amount of plugin lipstick changes that underlying model. Your ERP tracks stock across 3, 5, or 20 locations. Bridging the gap is not a plugin decision, it is an architecture decision — about how inventory is aggregated, how orders are routed, and what number a customer is allowed to see. Get those three right and the rest is plumbing.

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%)

Here is my blunt recommendation before you read another line: if you are not sure which of these to pick, pick “available minus buffer.” Overselling costs you a cancelled order, an apology email, and a customer who now doubts your stock numbers forever. Underselling costs you nothing you can measure. When the failure modes are that asymmetric, you bias toward the conservative number and stop agonizing.

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. The single most important thing to internalize here is direction of authority: the ERP is the source of truth for stock, WooCommerce is a display cache of one derived number, and the middleware is the only place allowed to reconcile the two. The moment you let a second system write stock authoritatively, you have two sources of truth and no truth at all.

I have watched this exact problem before

Long before ERP connectors, I built multi-user games in Macromedia Director and later Flash — the kind where a dozen people share one world and each one thinks they are holding the last sword in the armory. The bug that taught me the most was two players “buying” the same single item within the same tick because each client read the inventory, decided it was available, and committed independently. On screen both got the sword. In the authoritative state only one could exist. Somebody’s client had to be told, after the fact, that the thing they were promised was gone.

Multi-warehouse WooCommerce is that bug wearing a business suit. Two customers add the last unit of SKU-1234 to their carts because the displayed number said “in stock.” Both check out. Your ERP only had one. This is why I treat the displayed number as a hint and the ERP commit as the truth, and why I never let the storefront be the thing that decides an order can be fulfilled. Read is cheap and optimistic; the write is where reality is enforced. Design as if two people will always race for the last unit, because on a busy store, eventually they will.

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'

};

}

This is the strategy I reach for most often on real stores. It gives the storefront a stable number to render, keeps a physical buffer for your retail and wholesale channels that are also drawing down the same shelves, and it fails safe: the worst case is you show slightly less than you truly have.

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. I would not build this until the simpler strategies are running in production and the order data proves regional splitting actually changes fulfillment cost. It is the most impressive to demo and the most expensive to maintain, and those two facts are rarely a coincidence.

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

What I Would Actually Build First

If you are standing at the start of this project, resist the instinct to design the whole scoring engine on day one. Here is the sequence I would ship, in order, and I would not start a step until the previous one had run clean in production for a week:

  • One-way stock sync, primary warehouse plus buffer. ERP writes one aggregated number into WooCommerce on a schedule. No routing yet. This alone eliminates most overselling.
  • Manual routing. Orders drop into the ERP and a human assigns the warehouse. Boring, but it hands you real data on which warehouse would have been chosen before you automate the choice.
  • Automated single-warehouse routing. Now encode the scoring, but only for orders one warehouse can fully fulfill. Watch the routing-efficiency metric in the table above.
  • Split shipments. Add this last, when the split-shipment rate is high enough to justify the shipping cost you will absorb. On many stores that day never arrives, and that is a perfectly good outcome.

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 — and in my experience the elaborate routing engine built on day one is almost always tuned for orders the store never actually receives.

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Search Window