The most expensive architectural mistake in multi-channel ecommerce is having no system of record — or having three of them. When Shopify thinks it owns inventory, Amazon thinks it owns inventory, and your ERP thinks it owns inventory, you don't have three sources of truth. You have three sources of conflict. Every order, every stock update, every price change becomes a negotiation between three systems that all believe they're right.
For ecommerce operations in Mexico, Colombia, and the US running Shopify as their DTC storefront, Amazon (US or Amazon.com.mx) as their marketplace, and either Odoo or NetSuite as their ERP, this question isn't academic. It determines whether your inventory is accurate, whether your accounting reconciles, whether your warehouse ships the right quantities, and whether your ops team spends their time growing the business or manually fixing discrepancies.
This post answers the question definitively: how to choose your system of record, what the rules are for each data domain, and how to architect the sync layer that enforces those rules across Shopify, Amazon, and your ERP.
What "System of Record" actually means
A system of record is the system whose value wins in any conflict. If Shopify says you have 10 units and your ERP says 8, the system of record's number is correct — the other one is stale and must be updated. There is no averaging, no manual review, no "let's check both." The SoR decides.
The challenge is that different data domains have different natural owners. Inventory is not the same as orders. Orders are not the same as product catalog. Product catalog is not the same as pricing. Your SoR architecture must define ownership at the domain level, not at the system level. Trying to make one system own everything is where most integrations go wrong.
Owner: ERP (Odoo or NetSuite)
Your ERP tracks every physical movement of stock — receipts from suppliers, transfers between warehouses, adjustments, cycle count results. It's the only system connected to your warehouse or 3PL. Shopify and Amazon read from it; they never write to it except via the integration layer.
Owner: Channel (where the sale happened)
Shopify owns Shopify orders. Amazon owns Amazon orders. They push to the ERP for fulfillment. The ERP never creates an order back to the channel — it only confirms fulfillment. Orders are a one-way flow: channel → ERP.
Owner: ERP (master SKU) + channels (presentation)
Your ERP owns the canonical SKU, product name, and cost. Shopify and Amazon own the presentation layer — titles, descriptions, images, bullet points. A product SKU change in the ERP propagates to channels. A Shopify title change does not propagate back to the ERP.
Owner: ERP (cost + margin rules) → channel-specific price lists
Your ERP owns cost and target margin. The integration layer computes channel-specific prices by applying channel rules (Amazon fees, Shopify DTC margin, MELI commission in LATAM). No channel ever sets a price that propagates back to the ERP's cost or margin targets.
Owner: Amazon / MELI (for their FC stock) — read-only to ERP
Units physically at Amazon FBA or MELI FULL fulfillment centers are owned by those platforms until sold. Your ERP tracks them as a separate location (read-only) — it cannot instruct Amazon or MELI to move, adjust, or transfer this stock. These units are never included in Shopify's available inventory.
Owner: ERP (always, no exceptions)
Revenue, cost of goods, accounts receivable, accounts payable — all owned by the ERP. Shopify Payments, Amazon Seller Central, and Mercado Pago each have their own financial summaries, but they are inputs to the ERP's accounting, not replacements for it. For LATAM operations, CFDI (Mexico), DIAN (Colombia), and Nota Fiscal (Brazil) are all generated from the ERP.
The three candidate architectures
Teams running Shopify, Amazon, and an ERP typically evolve through three architectural phases, usually in this order. Understanding why the first two fail is what makes the third one stick.
Shopify as SoR
Works when you only sell on Shopify. Breaks the moment Amazon FBA stock diverges from your warehouse stock, or when a purchase order receipt needs to be reflected across channels simultaneously. Shopify has no concept of multi-warehouse, no cost tracking, no supplier management.
Amazon Seller Central as SoR
Amazon's inventory model tracks FBA stock only — it has no visibility into your warehouse, your Shopify-fulfilled stock, or your MELI inventory. Using Amazon as SoR means your Shopify inventory is perpetually wrong and your accounting is a disaster. This architecture exists only accidentally.
ERP (Odoo / NetSuite) as SoR
Your ERP connects to your warehouse, your suppliers, your 3PL, and your accounting. It's the only system that has full visibility into all stock movements — not just sales. It generates the financial documents required for LATAM compliance. Every channel reads from it; none writes back to it directly.
Odoo vs NetSuite: choosing your ERP SoR for LATAM + US operations
| Dimension | Odoo 16 / 17 | NetSuite |
|---|---|---|
| LATAM localization | Strong — Mexico CFDI 4.0, Colombia DIAN, Brazil Nota Fiscal available as community/OCA modules | Available via SuiteApp — less mature for Mexico/Colombia than Odoo |
| Multi-currency (ARS volatility) | Native multi-currency with daily rate updates | Native multi-currency with automated exchange rate feeds |
| Multi-company (MX + CO subsidiaries) | Native multi-company in Odoo 16+ | Native subsidiary management — better for large orgs |
| API for integration | XML-RPC + REST (Odoo 17) — well-documented, community Python libraries | REST + SuiteQL + SuiteScript — powerful but steeper learning curve |
| Total cost (SMB LATAM brand) | $300–$800/mo — open source, self-hosted or Odoo.sh | $2,000–$5,000+/mo — enterprise licensing |
| US cross-border ops | Functional but requires more configuration | Strong — widely used by US brands with LATAM subsidiaries |
| Molten Logistics recommendation | LATAM-first SMB/mid-market brands (<$20M revenue) | US-first enterprise brands with LATAM expansion (>$20M revenue) |
The sync rules that enforce your SoR
Deciding your ERP is the system of record is easy. Enforcing it across three channels, two fulfillment models (self-fulfilled and FBA/FULL), and multiple countries is where most teams struggle. These rules make it concrete.
Inventory flows outward from the ERP — never inward from channels
The ERP's qty_available (Odoo) or quantityAvailable (NetSuite) is the authority. Every 15 minutes, push the per-channel safe quantity to Shopify and Amazon. Every sale on any channel triggers an immediate event-driven push to all other channels. No channel write-back to ERP stock except through the integration layer's reconciliation process.
SAFETY_BUFFER = 5 # never push 100% of available AMAZON_EXTRA = 3 # FBM-specific extra buffer — FBA is read-only def push_inventory_all_channels(sku: str, odoo_wh_id: int, shopify_variant_id: int, shopify_location_id: int, amazon_seller_sku: str, is_fbm: bool): """ Reads authoritative qty from Odoo and pushes to all channels. FBA listings are NEVER pushed to — Amazon manages FBA qty. """ models, uid = odoo_connect() # Step 1: Get Odoo available qty at selling warehouse (excludes FBA/FULL locations) quants = models.execute_kw(ODOO_DB, uid, ODOO_KEY, "stock.quant", "search_read", [[("product_id.default_code", "=", sku), ("location_id.usage", "=", "internal"), ("location_id.warehouse_id", "=", odoo_wh_id), ("location_id.x_is_fba_location", "=", False)]], {"fields": ["quantity", "reserved_quantity"]} ) available = sum(q["quantity"] - q["reserved_quantity"] for q in quants) base_qty = max(0, int(available) - SAFETY_BUFFER) # Step 2: Push to Shopify requests.post( f"https://{SHOPIFY_SHOP}.myshopify.com/admin/api/2024-04/inventory_levels/set.json", headers=shopify_headers(), json={"location_id": shopify_location_id, "inventory_item_id": shopify_variant_id, "available": base_qty} ) # Step 3: Push to Amazon FBM only (never push to FBA listings) if is_fbm: fbm_qty = max(0, base_qty - AMAZON_EXTRA) sp_api.patch( f"/listings/2021-08-01/items/{AMAZON_SELLER_ID}/{amazon_seller_sku}", params={"marketplaceIds": AMAZON_MARKETPLACE_ID}, body={"productType": "PRODUCT", "patches": [{ "op": "replace", "path": "/attributes/fulfillment_availability", "value": [{"fulfillment_channel_code": "DEFAULT", "quantity": fbm_qty}] }]} )
Orders flow inward to the ERP from all channels
Every sale on Shopify, Amazon, or Mercado Libre creates a record in the ERP. The ERP owns fulfillment. The ERP confirms shipment. The channel receives the tracking confirmation. This is a strict one-way flow for orders — and it is the most important rule to enforce because violations create duplicate shipments, under-shipments, and accounting gaps.
- Shopify orders → Odoo
sale.ordervia webhookorders/paid - Amazon orders → Odoo
sale.ordervia SP-API Orders endpoint polling + webhook - MELI orders → Odoo
sale.ordervia Notifications APIorders_v2 - ERP ships → ERP creates
stock.picking→ validates → pushes tracking to originating channel - No channel creates its own fulfillment record in the ERP — the ERP's warehouse module is the only source of fulfillment truth
res.partner before the sale order is confirmed.
FBA and FULL inventory are tracked as read-only ERP locations
Amazon FBA units and MELI FULL units are not in your warehouse. Your ERP must track them as separate stock locations — not as warehouse stock. This is non-negotiable for accurate financial reporting and correct channel inventory.
| Location type | Odoo location name | Sync direction | Shopify includes? | MELI non-FULL includes? |
|---|---|---|---|---|
| Your warehouse (MX) | WH/Stock — México | ERP → all channels | ✓ Yes | ✓ Yes |
| Your warehouse (CO) | WH/Stock — Colombia | ERP → CO channels | ✓ Yes (CO location) | ✓ Yes (MCO) |
| Amazon FBA — US | Amazon FBA US (virtual) | Amazon → ERP (read) | ✗ Never | ✗ Never |
| Amazon FBA — MX | Amazon FBA México (virtual) | Amazon → ERP (read) | ✗ Never | ✗ Never |
| MELI FULL — CDMX | MELI FULL CDMX (virtual) | MELI → ERP (read) | ✗ Never | ✗ Never (separate FULL listing) |
| In-transit to FBA | WH/Output → FBA transit | ERP internal | ✗ Never | ✗ Never |
Product catalog changes flow from ERP outward — never reverse
Your ERP owns the canonical SKU and product data. When a product is created, renamed, or discontinued in the ERP, that change propagates to channels. When a Shopify product description is updated, it stays on Shopify — it does not update the ERP product name.
The exception: product attributes specific to each channel (Amazon bullet points, MELI item attributes, Shopify SEO description) are maintained in the channel directly. Never try to sync these back to the ERP — they don't belong there and the mapping will fail.
def on_erp_product_updated(odoo_product_id: int, changed_fields: list[str]): """ Called when product.template is written in Odoo. Only propagates fields that channels care about. """ models, uid = odoo_connect() product = models.execute_kw(ODOO_DB, uid, ODOO_KEY, "product.template", "read", [odoo_product_id], {"fields": ["name", "default_code", "list_price", "active", "x_meli_item_id", "x_shopify_product_id", "x_amazon_seller_sku"]} ) # Fields that propagate to Shopify shopify_propagate = {"list_price", "active"} # NOT name — Shopify owns its own title if shopify_propagate & set(changed_fields): shopify_update_product( product_id=product["x_shopify_product_id"], price=product["list_price"], active=product["active"], ) # Fields that propagate to Amazon FBM amazon_propagate = {"list_price", "active"} if amazon_propagate & set(changed_fields) and product["x_amazon_seller_sku"]: amazon_update_listing( seller_sku=product["x_amazon_seller_sku"], price=compute_amazon_price(product["list_price"]), ) # Fields that propagate to MELI meli_propagate = {"list_price"} if meli_propagate & set(changed_fields) and product["x_meli_item_id"]: meli_update_price( item_id=product["x_meli_item_id"], price=compute_meli_price(product["list_price"]), )
Accounting and financial reporting live exclusively in the ERP
Amazon Seller Central has revenue reports. Shopify has a P&L. Mercado Pago has transaction summaries. None of these replace your ERP's accounting module. They are data sources that feed into it.
For Mexico, every invoice generated for a Shopify or MELI sale must originate from the ERP's CFDI 4.0 module — not from Shopify's invoice generator or from MELI's internal billing. For Colombia, DIAN e-invoicing is the same. For Brazil, Nota Fiscal. The ERP is the legal accounting system of record for LATAM operations — there is no alternative.
- Shopify Payments daily payout → reconcile against Odoo
account.paymentjournal entries - Amazon disbursements (2x/month) → reconcile against ERP AR for Amazon channel
- Mercado Pago balance → reconcile against ERP AR for MELI channel
- Mexico: CFDI 4.0 generated from ERP per order — required for all B2C and B2B sales
- Colombia: DIAN electronic invoice generated from ERP — mandatory for registered businesses
Daily reconciliation confirms your SoR is actually enforced
Declaring the ERP as your system of record and actually enforcing it are two different things. The only way to know the rules are holding is a daily reconciliation job that compares ERP data against every channel and flags variances. This is your audit trail and your early warning system.
def daily_sor_reconciliation() -> dict: """ Runs daily. Compares ERP state against all channels. Returns dict of discrepancies for ops dashboard. """ report = {"inventory_drift": [], "order_gaps": [], "price_mismatches": []} active_skus = get_all_active_skus_from_erp() for sku in active_skus: erp_qty = get_odoo_available(sku) shopify_qty = shopify_get_inventory(sku) amazon_qty = amazon_get_fbm_qty(sku) # FBM only # Inventory drift check (allow SAFETY_BUFFER variance) for channel, ch_qty in {"shopify": shopify_qty, "amazon_fbm": amazon_qty}.items(): expected = max(0, erp_qty - SAFETY_BUFFER) drift = abs(expected - ch_qty) if drift > 3: # more than 3 units drift → investigate report["inventory_drift"].append({ "sku": sku, "channel": channel, "erp_qty": erp_qty, "channel_qty": ch_qty, "drift": drift }) # Order gap check: ERP orders today vs channel order count today erp_orders = get_erp_orders_today_for_sku(sku) shopify_orders = shopify_orders_today(sku) amazon_orders = amazon_orders_today(sku) if len(erp_orders) < len(shopify_orders) + len(amazon_orders): report["order_gaps"].append({ "sku": sku, "erp_count": len(erp_orders), "channel_total": len(shopify_orders) + len(amazon_orders), }) return report
SoR rules specific to LATAM operations
The ERP-as-SoR architecture has additional requirements when your operations span Mexico, Colombia, Argentina, or Brazil. These are not optional enhancements — they're legal and operational requirements that don't exist for pure US operations.
| Country | ERP SoR requirement | Why it matters | Fails without ERP? |
|---|---|---|---|
| 🇲🇽 Mexico | CFDI 4.0 e-invoice from ERP for every sale | Legal requirement for all commercial transactions — SAT compliance | Yes — Shopify/MELI cannot generate CFDI |
| 🇨🇴 Colombia | DIAN electronic invoice from ERP | Mandatory for registered companies — all B2B and B2C above threshold | Yes — channels don't generate DIAN documents |
| 🇧🇷 Brazil | Nota Fiscal from ERP for every outbound shipment | Required for shipping — carrier won't pick up without NF | Yes — Shopify cannot generate Nota Fiscal |
| 🇦🇷 Argentina | ARS price stored as USD equivalent in ERP; converted at push time | ARS volatility makes fixed ARS prices economically wrong within 30 days | Partially — prices drift without ERP as master |
| 🇲🇽🇨🇴 MX + CO | Separate ERP operating units per country | Different tax rates, currencies, fiscal positions, chart of accounts | Yes — commingled accounting is legally invalid |
| 🇺🇸→🇲🇽 US cross-border | HS codes on all products in ERP — required for MELI CBT customs clearance | Missing HS codes cause customs holds and delayed LATAM deliveries | Yes — channels don't manage HS codes |
SoR implementation checklist
🏗️ Architecture Decisions
- ERP declared as system of record — documented and agreed by engineering, ops, and finance teams
- Data domain ownership table published: who owns inventory, orders, catalog, pricing, accounting
- No channel has direct write-back to ERP inventory outside of the integration layer
- FBA and MELI FULL tracked as separate read-only ERP locations — never mixed with warehouse stock
- For LATAM: separate Odoo operating unit (or NetSuite subsidiary) per country with legal entity
📦 Inventory Enforcement
- Scheduled inventory push: ERP → Shopify + Amazon FBM every 15 minutes
- Event-driven push fires on every order webhook from any channel
- Safety buffer (5 units base + channel-specific extra) applied before every channel push
- Shopify inventory never includes Amazon FBA or MELI FULL stock
- Amazon FBA inventory synced to ERP FBA location daily — read only, no push back to Amazon
🛒 Order Flow
- All channel orders create ERP sale.order records within 2 minutes of placement
- Idempotency enforced: client_order_ref checked before any ERP order creation
- ERP is the only system that creates stock.picking — no channel creates its own fulfillment
- Fulfillment confirmation pushed from ERP back to originating channel on stock.picking done
- Mexico B2B orders: RFC captured at order creation and stored on res.partner
- Colombia B2B orders: NIT captured at order creation and stored on res.partner
💰 Accounting & LATAM Compliance
- Mexico: CFDI 4.0 module configured in Odoo — every sale generates a valid XML fiscal document
- Colombia: DIAN e-invoicing module active — every sale generates electronic invoice
- Brazil: Nota Fiscal module configured — every outbound shipment generates NF before carrier pickup
- Shopify Payments reconciled against Odoo bank statement daily
- Amazon disbursements reconciled against Odoo AR for Amazon channel twice monthly
- Mercado Pago balance reconciled against Odoo AR for MELI channel weekly
🔍 Monitoring & Reconciliation
- Daily SoR reconciliation job: ERP qty vs every channel qty — drift > 3 units triggers alert
- Daily order gap check: channel order count vs ERP sale.order count — gap triggers immediate investigation
- Weekly price audit: ERP list price × channel rule = expected channel price — mismatch flagged
- Monthly FBA/FULL reconciliation: ERP FBA/FULL location vs Amazon/MELI reported stock
- Runbook documented: how to force-resync any data domain from ERP on demand
Your ERP is ready to be the boss
The system of record decision is an architecture decision, not a tool decision. Odoo and NetSuite are both capable of being the SoR for a multi-channel operation running Shopify, Amazon, and Mercado Libre. What determines success is not which ERP you choose — it's whether you enforce the ownership rules consistently across every data domain, every channel, and every country.
For US brands entering Mexico and Colombia, the fiscal localization layer is what most teams underestimate. CFDI 4.0 and DIAN e-invoicing are not add-ons — they're core infrastructure for your LATAM operation. Getting them right in your ERP before your first LATAM sale is far cheaper than retrofitting them after 10,000 transactions are already in the system with missing fiscal data.
Built this kind of architecture — and run it in production
Luis Alba and the Molten team have set up ERP-as-SoR architectures for Shopify + Amazon + Mercado Libre operations across Mexico, Colombia, and the US. From Odoo CFDI 4.0 setup to NetSuite multi-subsidiary configuration to the integration layer that enforces every SoR rule in production.
See if you qualify →