The call that comes at month three
A Shopify merchant with three fulfillment warehouses — one in New Jersey, one in Texas, one in the UK — deployed a basic Shopify Odoo connector and ran it for three months. Then they called their connector vendor. Inventory in Shopify was showing 800 units of their top SKU available. The actual physical count was 320. The other 480 existed in Odoo, spread across three warehouses, and the connector had been aggregating all three into Shopify's "Main Location" because it was configured for a single-warehouse setup.
When a customer ordered 200 units and the New Jersey warehouse went to pick them, they had 90. The order had to be split, partially cancelled, and partially refunded. Customer support spent two days on it.
This guide is about how to avoid that scenario. Multi-warehouse inventory sync is not a configuration toggle — it is an architecture decision that affects how you model your warehouses, how you write sync rules, and how you think about which system owns what.
---
The single-warehouse fallacy
Most Shopify Odoo connectors are built around a single-warehouse assumption. The connector takes the available quantity from one Odoo warehouse's stock.location (or the computed qty_available from product.product) and pushes it to the corresponding Shopify product's inventory level for the "Default" location.
This works perfectly for:
- A merchant with one physical warehouse
- A merchant who does drop-shipping from one supplier
- A small operation where all stock is in one place
It fails completely for:
- Any merchant with more than one Shopify location enabled
- Any merchant using 3PL (third-party logistics) with multiple nodes
- Any merchant who does cross-docking or multi-step routes in Odoo
- Any merchant with a retail store that also sells online and shares inventory
The failure mode is not an error message — it's silent inventory drift. The connector keeps running. Orders keep coming in. The mismatch builds slowly, invisible until a picking failure or a customer complaint makes it visible.
---
Shopify locations vs Odoo warehouses: the mental model
Before designing any sync architecture, you need a clear mental model of how the two systems structure inventory.
Shopify locations
Shopify's inventory model is location-based. Every product variant has an inventory level per location. A Shopify location can represent:
- A physical warehouse
- A retail store
- A fulfillment service (like Amazon FBA as a Shopify location)
- A "virtual" location used for dropship suppliers
Every location in Shopify has a unique location_id. Inventory levels are queried as InventoryLevel records that link a location_id to an inventory_item_id (which is linked to a product variant).
Key Shopify constraint: Shopify does not have sub-locations. A Shopify location is a single node. There is no concept of "bin A3" or "aisle 7" inside a Shopify location. All of that granularity lives in the warehouse management system, not Shopify.
Odoo warehouses and locations
Odoo's stock model is a directed acyclic graph of locations. Every warehouse has:
- A stock location (e.g.,
WH/Stock) — the main storage location - An input location (where goods are received)
- An output location (where goods are staged for delivery)
- A packing location (for multi-step routes)
- A quality control location (optional)
Within the stock location, you can create sub-locations (bins, rows, zones) to any depth. WH/Stock/Zone-A/Bin-A3 is a valid Odoo location.
Inventory in Odoo is tracked as stock.quant records — each quant links a product_id, a location_id, and a quantity. The system quantity available for sale is the sum of all quants in locations that are considered internal type and part of the warehouse's available stock.
Odoo can also exclude certain internal locations from availability calculations using route configurations and removal strategies. This is useful for quarantine stock, defective inventory, or reserved stock for B2B customers.
The critical insight: Odoo's available quantity for a product is a computed value across many storage locations. Shopify's inventory level is a single integer per location.
---
Mapping patterns
There are three fundamental mapping patterns for multi-warehouse sync.
Pattern 1: One-to-one mapping (most common)
One Shopify location maps to one Odoo warehouse.
``` Shopify Location: "New Jersey Warehouse" (location_id: 12345) → Odoo Warehouse: "NJ" (stock location: WH_NJ/Stock)
Shopify Location: "Texas Warehouse" (location_id: 67890) → Odoo Warehouse: "TX" (stock location: WH_TX/Stock)
Shopify Location: "UK Warehouse" (location_id: 11223) → Odoo Warehouse: "UK" (stock location: WH_UK/Stock) ```
For each Shopify inventory update or each Odoo stock change, the connector knows exactly which Shopify location to update based on which Odoo warehouse the change occurred in.
Configuration table (stored in connector):
``json [ { "shopify_location_id": "12345", "shopify_location_name": "New Jersey Warehouse", "odoo_warehouse_id": 1, "odoo_stock_location_id": 8, "odoo_warehouse_code": "WH_NJ" }, { "shopify_location_id": "67890", "shopify_location_name": "Texas Warehouse", "odoo_warehouse_id": 2, "odoo_stock_location_id": 15, "odoo_warehouse_code": "WH_TX" } ] ``
Sync direction rule: For each mapping, when Odoo's available quantity for product_id at odoo_warehouse_id changes, push to the matching shopify_location_id. One Shopify API call per changed location.
This pattern is simple and robust. The main risk is inconsistency when a product exists in multiple warehouses and Shopify only shows a total. In that case, you need pattern 3 below.
Pattern 2: One-to-many (Shopify location → multiple Odoo bins)
One Shopify location aggregates inventory from multiple Odoo sub-locations within one warehouse. This is common when Odoo uses zones for different types of stock (saleable, quarantine, defective) and only the saleable portion should be reflected in Shopify.
`` Shopify Location: "Main Warehouse" → Odoo Location: WH/Stock/Saleable (only this zone) → Odoo Location: WH/Stock/Zone-B (also included) → NOT Odoo Location: WH/Stock/Quarantine (excluded) ``
For this pattern, the connector must compute the available quantity as the sum of quants across the included Odoo sub-locations, excluding the quarantine or otherwise restricted zones.
``python def get_available_qty(client, product_id, included_location_ids): quants = client.search_read( 'stock.quant', [ ['product_id', '=', product_id], ['location_id', 'in', included_location_ids], ['location_id.usage', '=', 'internal'] ], fields=['quantity', 'reserved_quantity', 'location_id'] ) available = sum( q['quantity'] - q['reserved_quantity'] for q in quants ) return max(0, available) # never push negative to Shopify ``
The challenge with this pattern is change detection. When any quant in any of the included locations changes, you need to recompute the aggregate and push to Shopify. A webhook-only approach to change detection will miss quant changes that happen from internal moves (e.g., a quality inspector moving stock from quarantine to saleable without any Shopify-visible event). You need a periodic reconciliation job to catch these.
Pattern 3: Many-to-one (multiple Shopify locations → one Odoo warehouse)
This is the 3PL case. A single Odoo warehouse (managed by a 3PL provider) serves multiple Shopify locations, or a single Shopify store location receives fulfilled inventory from multiple Odoo warehouses.
`` Shopify Location: "Online Store" (location_id: 99999) ← Odoo Warehouse: "WH_NJ" (contributes) ← Odoo Warehouse: "WH_TX" (contributes) ← Odoo Warehouse: "WH_UK" (contributes, but only for EU orders) ``
This pattern is the most complex because you need routing logic: which Odoo warehouse should fulfill an order for a specific shipping address? The available quantity shown in Shopify needs to reflect the combined available stock across all contributing warehouses, but the fulfillment routing must happen in Odoo (or in the connector).
---
The 3PL case study walkthrough
Consider a merchant with these characteristics:
- Shopify Plus store
- $8M annual revenue
- Single Shopify location ("Main Location") that represents their virtual inventory pool
- Two 3PL warehouses (East Coast and West Coast) managed in Odoo
- 3PL provider gives daily stock reports via EDI; Odoo receives these and updates quants
- Orders are routed to the nearest 3PL based on shipping zip code
The inventory sync setup
The connector maintains a single Shopify location mapping to the aggregate of both Odoo warehouses:
``json { "shopify_location_id": "main", "odoo_warehouse_ids": [1, 2], "aggregation_rule": "sum", "safety_buffer_pct": 5 } ``
The 5% safety buffer prevents overselling in the time lag between when the 3PL processes a shipment and when Odoo's quant is updated. If East Coast warehouse shows 100 units, the connector pushes 95 to Shopify.
The order routing setup
When an order arrives from Shopify, the connector reads the shipping address. Based on zip code, it routes to either the East or West Coast Odoo warehouse and creates the sale order there. The delivery (stock.picking) is created against that warehouse's stock.
The reconciliation job
Every night at 2am, the connector: 1. Reads all product quantities from both Odoo warehouses 2. Aggregates with safety buffer 3. Compares to current Shopify inventory levels 4. Pushes updates for any product where the difference exceeds 2 units
This nightly reconciliation is the backstop against drift. During the day, webhook-driven updates handle real-time changes. The nightly job catches anything the webhooks missed.
---
Drop-shipping locations: when one Shopify location is actually multiple suppliers
Some merchants use a single Shopify location to represent inventory that is actually at multiple drop-shipping suppliers. Each supplier has their own stock pool. The merchant never touches the physical goods.
In Odoo, each supplier drop-ship arrangement maps to a stock.location of type supplier or a virtual warehouse. The connector needs to:
1. Know which Odoo product is sourced from which drop-ship supplier 2. Query each supplier's available quantity separately 3. Aggregate the minimum available across suppliers (since a drop-ship order typically goes to one supplier per order)
The most important rule for drop-ship inventory: never aggregate quantities across multiple drop-ship suppliers and present the sum as available. A customer who orders 10 units that are 6 at supplier A and 4 at supplier B cannot have that order fulfilled without splitting. Instead, show only the maximum single-supplier quantity as available, or show the minimum across all suppliers who carry the product.
---
Buffer stock and safety stock: how to model in Odoo, surface in Shopify
Odoo has native safety stock support through the orderpoint (reorder rules) system. You can set a minimum quantity on hand for each product at each warehouse. But this safety stock is a replenishment trigger — it doesn't automatically subtract from the quantity shown in Shopify.
To model safety stock correctly for Shopify:
Option A: Reserved location
Create an Odoo virtual location called WH/Stock/Safety-Reserve. Maintain a permanent internal move that keeps N units in that location. Configure the connector to exclude Safety-Reserve from the available quantity calculation.
This approach is explicit and auditable. The downside is that you need to maintain the reserve quant manually or via a scheduled action.
Option B: Buffer in the connector
The connector subtracts a configurable buffer from the Odoo available quantity before pushing to Shopify:
``python def compute_shopify_available(odoo_qty, buffer_config, product_id): buffer = buffer_config.get(product_id, buffer_config.get('default', 0)) return max(0, odoo_qty - buffer) ``
This is simpler but less visible to the Odoo warehouse team. The buffer only exists in the connector configuration, not in Odoo's stock records.
Option C: Shopify inventory policy
Shopify's "continue selling when out of stock" and "stop selling when out of stock" policies can be used to effectively set a buffer of zero — you only sell what Odoo says is available. This is the simplest approach but gives you no buffer for processing lag.
Most multi-warehouse operations use Option B (connector buffer) with buffer percentages that account for typical processing lag in their operation.
---
Periodic reconciliation: why webhook-only sync drifts over weeks
Webhooks from both Shopify and Odoo are not guaranteed delivery. Shopify's webhook system retries failed deliveries five times over 48 hours and then drops the event. Odoo does not have a native outbound webhook system — connector implementations typically poll for changes or use custom triggers.
In a multi-warehouse operation, drift sources include:
1. Internal Odoo moves — stock transfers between locations or warehouses that don't generate a Shopify-side event 2. Manual inventory adjustments — an Odoo user doing a physical count adjustment 3. Webhook delivery failures — network issues, connector downtime, Shopify rate limiting 4. Returned stock processing — a returned item put back into stock in Odoo without a corresponding Shopify return 5. EDI imports — 3PL stock feeds loaded into Odoo outside the normal order flow
A webhook-only connector, even a very reliable one, will show measurable drift after 3-4 weeks of operation in a busy multi-warehouse environment. Drift of 2-5% per month is typical for webhook-only approaches.
The solution is a scheduled full reconciliation job that runs at least daily:
```python async def full_inventory_reconciliation(shop, warehouse_mapping): for mapping in warehouse_mapping:
Get all products with inventory in this Odoo warehouse
odoo_quantities = get_all_product_quantities( mapping['odoo_warehouse_id'] )
Get all current Shopify inventory levels for this location
shopify_levels = get_shopify_inventory_levels( mapping['shopify_location_id'] )
Compute deltas
updates = [] for product_id, odoo_qty in odoo_quantities.items(): shopify_qty = shopify_levels.get(product_id, 0) adjusted_qty = apply_buffer(odoo_qty, mapping) if abs(adjusted_qty - shopify_qty) > 1: # ignore 1-unit rounding updates.append({ 'inventory_item_id': product_id, 'location_id': mapping['shopify_location_id'], 'available': adjusted_qty })
Batch update Shopify (max 100 per API call)
await batch_update_shopify_inventory(updates) ```
---
Sync direction rules: who owns the truth for which event
This is the most important architectural decision for multi-warehouse sync. Write down explicit rules and document them in your connector configuration.
Odoo is source of truth for:
- Available quantity after receiving a purchase order
- Available quantity after a physical inventory count
- Available quantity after a return is processed and restocked
- Reserved quantity (units committed to open orders)
Shopify is source of truth for:
- The fact that an order was placed (triggers Odoo to reserve stock)
- Customer-initiated cancellations before fulfillment (triggers Odoo to release reservation)
Neither system should update inventory during:
- A transit between warehouses (in-transit quantity is tracked in Odoo but should not appear in Shopify as available until received at destination)
Write these rules into your connector's configuration as explicit policies, not implicit behavior:
``json { "sync_direction": { "inventory_quantity": "odoo_to_shopify", "reservation": "shopify_triggers_odoo", "in_transit_visibility": "exclude_from_shopify" } } ``
---
Migration path: single-warehouse to multi-warehouse without downtime
If you are currently running a single-warehouse Shopify Odoo connector and need to migrate to multi-warehouse support, follow this sequence:
Step 1: Freeze inventory sync (30 minutes)
Pause the connector's outbound inventory sync. Note the exact Shopify inventory levels for all products at the current single location. Do not make any changes to Shopify inventory during this window.
Step 2: Set up Odoo warehouses and locations
In Odoo, verify that all the warehouses you want to sync are configured and have inventory. Run a physical count or verify that the counts are accurate before proceeding.
Step 3: Create the new location mapping configuration
Add the new warehouse-to-location mapping entries to the connector configuration. Do not activate them yet.
Step 4: Set up the new Shopify locations
In Shopify, create the new locations (if they don't already exist). Assign inventory to these locations. Shopify's inventory can be split across locations by transferring from the "Main" location to the new ones.
Step 5: Run a reconciliation dry-run
With the connector paused, run the reconciliation logic in read-only mode. It should report the expected quantity for each product at each location. Review these numbers.
Step 6: Activate the new mapping
Enable the new multi-warehouse sync configuration. Run the first full reconciliation. This will update Shopify inventory levels to match Odoo's warehouse-accurate quantities.
Step 7: Verify and monitor for 24 hours
Check the 10-20 most important SKUs manually. Verify that order routing in Odoo is correctly assigning fulfillment to the right warehouse based on the shipping address. Monitor for any alert conditions (see below).
---
Monitoring: what to alert on, what to check weekly
Alert immediately on:
- Negative inventory in Shopify — Any product showing negative inventory in Shopify means the connector pushed a negative value (a bug) or Shopify oversold. Alert and investigate within minutes.
- Drift > 20% on any high-velocity SKU — If a product that typically sells 50 units/day shows a >20% drift between Odoo and Shopify, alert. This indicates a stuck webhook or a processing failure.
- Sync job failure — Any failure of the reconciliation job should alert the operations team.
Check weekly:
- Total inventory value: Odoo vs Shopify — The dollar value of inventory should match between systems. A growing gap (more than 1-2%) indicates systematic drift.
- Products with zero Shopify availability but positive Odoo stock — This means products are being suppressed from sale that could be selling.
- Products with Shopify availability but zero Odoo stock — This is the worst case: oversell risk. Investigate immediately.
- Buffer utilization — Are your safety buffers actually being used? If the buffer is set to 50 units but stock never drops to within 50 of zero, the buffer is too large and hiding real available inventory.
Building a weekly digest report from the connector into a Slack channel or email takes a few hours to set up and prevents the three-month surprise described at the top of this guide.
---
Summary
Multi-warehouse Shopify Odoo inventory sync requires explicit architecture decisions before writing a single line of configuration. The decisions that matter most:
1. Choose your mapping pattern (1:1, 1:many, many:1) before configuring anything 2. Define who owns the truth for each type of inventory event 3. Implement webhook-driven real-time sync AND a daily full reconciliation 4. Configure safety buffers in the connector, not as manual Odoo adjustments 5. Set up monitoring alerts for the failure modes that matter in your operation
Synco Connector supports all three mapping patterns, configurable per-product safety buffers, and built-in daily reconciliation jobs designed for multi-warehouse operations.
Install from the Shopify App Store or visit our multi-location inventory sync guide and the inventory sync reference for full configuration documentation.