Free reference · Updated 2026-08-09 · No signup
Odoo → Shopify field mapping
reference
Every field worth syncing between Odoo and Shopify — products, inventory, customers, orders — with the direction it should move in and the specific failure attached to getting it wrong. This is the sheet we work from internally. Copy it, cite it, argue with it.
Products and variants
SKU is the join key in each direction. If a SKU is blank, duplicated, or differs by whitespace or case, every downstream mapping in this table silently fails.
| Odoo field | Shopify field | Direction | What to watch |
|---|---|---|---|
| product.template.name | product.title | Odoo → Shopify | Odoo is the source of record for naming. Overwriting on every sync will undo manual Shopify edits — map once on create if merchandising owns titles. |
| product.product.default_code | variant.sku | Both ways | The join key. Trim whitespace and normalise case before matching; Shopify treats SKUs as free text and does not enforce uniqueness. |
| product.template.barcode | variant.barcode | Odoo → Shopify | Use as a secondary match key when default_code is missing on legacy Odoo records. |
| product.template.list_price | variant.price | Odoo → Shopify | Retail price only. B2B rates belong in a price list, not in variant.price — pushing customer-specific pricing here exposes it to every visitor. |
| product.template.standard_price | variant.inventoryItem.cost | Odoo → Shopify | Cost is not exposed on the storefront. Required if you want Shopify margin reporting to be meaningful. |
| product.template.weight | variant.inventoryItem.measurement.weight | Odoo → Shopify | Check the unit. Odoo stores in the company UoM; Shopify expects an explicit unit per variant. |
| product.template.description_sale | product.descriptionHtml | Odoo → Shopify | Odoo stores plain text, Shopify expects HTML. Escape before writing or line breaks disappear. |
| product.template.categ_id | product.productType / tags | Odoo → Shopify | Odoo categories are a tree, Shopify product type is a single string. Flatten to the leaf and mirror ancestors as tags if you need faceting. |
| product.attribute.value | product.options / variant.selectedOptions | Odoo → Shopify | Shopify allows a limited number of options per product. Odoo attribute sets that exceed it must be split into separate products. |
| product.template.active | product.status | Odoo → Shopify | Archive in Odoo should map to DRAFT, not delete. Deleting breaks order history references in Shopify. |
Inventory and locations
The single most common source of drift. Odoo reports several stock figures and only one of them is the number a buyer should see.
| Odoo field | Shopify field | Direction | What to watch |
|---|---|---|---|
| stock.quant.quantity | inventoryLevel.quantities(on_hand) | Odoo → Shopify | Physical stock, reservations included. Publishing this oversells whenever Odoo holds a reservation Shopify cannot see. |
| product.product.qty_available - outgoing_qty | inventoryLevel.quantities(available) | Odoo → Shopify | This is normally the figure you want. Free-to-use quantity after outgoing moves are deducted. |
| stock.warehouse / stock.location | location | Odoo → Shopify | Map one Odoo warehouse to one Shopify location explicitly. Implicit mapping by name breaks the first time someone renames a warehouse. |
| product.template.type = 'consu' | variant.inventoryItem.tracked = false | Odoo → Shopify | Non-stocked items must be untracked in Shopify, otherwise they show as sold out permanently. |
| stock.quant write_date | — (sync cursor) | Odoo → Shopify | Use a write_date cursor for incremental pulls. Full inventory sweeps will exhaust Shopify API credits on catalogues above roughly 5,000 variants. |
Customers and companies
Odoo models a company and its contacts in one table with a parent link. Shopify splits customers and companies into separate objects, so the mapping is not one-to-one.
| Odoo field | Shopify field | Direction | What to watch |
|---|---|---|---|
| res.partner (is_company = true) | company | Both ways | One Odoo company partner becomes one Shopify company. Match on VAT number or an external reference, never on name. |
| res.partner (parent_id set) | companyContact / customer | Both ways | Child contacts become company contacts. A contact with no email cannot be created in Shopify at all. |
| res.partner.email | customer.email | Both ways | Secondary join key. Duplicate emails across Odoo partners are the usual cause of merged or overwritten Shopify customers. |
| res.partner.vat | company.externalId / metafield | Odoo → Shopify | Store it as a stable external ID. It survives renames and is the only reliable long-term match key for B2B accounts. |
| res.partner.property_product_pricelist | catalog / price list assignment | Odoo → Shopify | Where Shopify's catalogue cap bites: Odoo allows unlimited pricelists, native Shopify B2B does not. Map to an uncapped pricing layer instead. |
| res.partner.property_payment_term_id | company.paymentTerms | Odoo → Shopify | Terms names must exist on the Shopify side first; unknown term templates are rejected rather than created. |
Orders and fulfilment
Direction matters more here than anywhere else. Pick one system as the order of record and let the other mirror it, or you will get duplicate stock moves.
| Odoo field | Shopify field | Direction | What to watch |
|---|---|---|---|
| sale.order | order / draftOrder | Shopify → Odoo | Storefront and portal orders originate in Shopify. Write them into Odoo as confirmed sale orders; keep the Shopify order ID on the Odoo record. |
| sale.order.name | order.note / metafield | Odoo → Shopify | Push the Odoo reference back so support staff can search either system with one number. |
| sale.order.line.price_unit | lineItem.originalUnitPrice | Shopify → Odoo | Send the resolved per-customer price, not the list price, or Odoo margin reporting is wrong on every B2B line. |
| stock.picking (done) | fulfillment | Odoo → Shopify | Fulfil in Shopify only when the Odoo picking is validated. Fulfilling on picking creation triggers customer emails for goods still in the warehouse. |
| stock.picking.carrier_tracking_ref | fulfillment.trackingInfo | Odoo → Shopify | Include the carrier name; Shopify only renders a tracking link for carriers it recognises. |
| account.move (invoice) | — (stays in Shopify or Odoo) | Odoo → Shopify | Do not attempt round-trip invoice sync. Choose one system to issue the invoice; mirroring both creates duplicate ledger entries. |
Free playbook
Want this mapping as a checklist you can tick off?
We send the Odoo → Shopify migration checklist — join keys, field owners, and the pre-flight tests to run before the first sync.
Six ways this goes wrong
The pitfalls behind most broken syncs
Matching on name instead of a key
Names change; SKUs and VAT numbers do not. Every mapping in the tables above uses a stable key for exactly this reason. A sync built on names works for three months and then quietly duplicates half your catalogue.
Publishing on_hand instead of available
Odoo's on-hand figure includes reserved stock. Shopify has no visibility of Odoo reservations, so it will happily sell units already allocated to a picking. Use available quantity.
Round-trip sync on fields only one system owns
Pick an owner per field. Product titles, costs and stock are owned by the ERP; order status and fulfilment state are owned by whichever system actually ships. Round-trip writes on a single-owner field cause update loops.
Ignoring API cost, not API count
Shopify's GraphQL Admin API bills by query cost against a leaky bucket, not by request count. A nightly full sweep that passes in staging with 200 products will throttle at 5,000. Sync incrementally against a write_date cursor.
No idempotency key on writes
Retries are normal. Without a deterministic key on the Shopify side — the SKU, the Odoo record ID in a metafield — a retried create becomes a duplicate rather than an update.
Currency and unit mismatch
Odoo stores prices in the company currency and weights in the company UoM. Shopify stores per-market currency and an explicit unit per variant. Convert at the mapping layer, not in a spreadsheet afterwards.
Cite this reference
This page is free to quote and link to, in full or in part, with attribution. No permission needed.
Hasil (2026). "Odoo → Shopify field mapping reference." https://hasil.io/odoo-shopify-field-mapping
<a href="https://hasil.io/odoo-shopify-field-mapping">Odoo → Shopify field mapping reference — Hasil</a>
FAQ
Mapping questions we get asked
What field should I use to match Odoo products to Shopify products?+
Use Odoo's default_code against Shopify's variant SKU, normalised for case and whitespace. Fall back to barcode against variant barcode for legacy records with no internal reference. Never match on product name.
Should I sync on-hand or available quantity from Odoo to Shopify?+
Available quantity — qty_available minus outgoing_qty. On-hand includes stock already reserved against Odoo pickings, which Shopify cannot see, so publishing it causes overselling.
How do Odoo pricelists map to Shopify B2B catalogues?+
They do not map cleanly. Odoo allows an unlimited number of pricelists per company, while native Shopify B2B caps catalogues and treats each one as a single pricing tier. Merchants with more pricing arrangements than catalogue slots need an uncapped per-customer price-list layer instead.
Which system should own the order record?+
Whichever system the order originates in. Storefront and B2B portal orders originate in Shopify and should be written into Odoo as confirmed sale orders, with the Shopify order ID stored on the Odoo record for reconciliation.
Why does my sync work in testing but throttle in production?+
Shopify's GraphQL Admin API charges by query cost against a leaky bucket rather than counting requests. Full catalogue sweeps that pass with a few hundred products exhaust the bucket at a few thousand. Sync incrementally using a write_date cursor.
Keep reading
Free playbook · 1 email/month
Not ready for a trial? Take the playbook instead.
Or skip the mapping sheet entirely
Hasil Sync ships these mappings as defaults — SKU matching, available-quantity inventory, draft orders back to Shopify — configured in about three minutes, no developer, no Zapier.
Start 14-day free trial