Implementation For Developers 10 min read Updated May 6, 2026

Odoo Data Migration: External IDs, Cutover, Validation, and Import Strategy

How to safely migrate data from legacy systems to Odoo — field mapping, validation, idempotent imports, and the cutover strategies that don't produce silent data loss.

An Odoo data migration looks deceptively simple: "export your data from the old system, import it into Odoo." The reality is that source data is almost always inconsistent, mappings are almost always non-trivial, and the order of operations determines whether the migration succeeds or fails silently. The hardest migration problems aren't the rows that fail to import (those get caught) — they're the rows that import successfully into the wrong place.

This guide is for developers and technical leads planning or executing an Odoo migration from a legacy ERP, a custom database, an e-commerce platform, or another business system. It covers strategy, the import mechanisms Odoo provides, validation patterns, and the cutover process — including the operational decisions that determine whether the migration is recoverable if something goes wrong.

Migration is an ETL project, not a data dump

The standard pattern for any Odoo migration:

  1. Extract from the source system into structured intermediate files (CSV, JSON, or a staging database).
  2. Transform the data to Odoo's expected shapes — field renaming, value normalization, reference resolution, calculated fields.
  3. Validate the transformed data against Odoo's constraints — required fields, valid foreign keys, value ranges.
  4. Load into Odoo via XML-RPC, JSON-RPC, the import wizard, or direct ORM calls in a custom module.

The Validate step is the one that separates migrations that ship successfully from ones that produce data quality problems for years. Source data has typos, dropped fields, mis-coded categories, mixed currencies, and date formats that look right but parse wrong. Validation catches all of these before they reach Odoo.

Important. Never load directly from source to Odoo without an intermediate staging step. Direct loads make iterating on transformations impossible and turn every error into a database rollback.

The migration order that always works

Migrate in dependency order. A record cannot reference data that doesn't exist yet.

  1. Configuration data: countries, states, currencies, languages — usually loaded by Odoo's localization, but verify before going further.
  2. Reference data: product categories, attribute definitions, account chart customizations, tax codes.
  3. Master data — partners: customers, suppliers, contact persons. Includes addresses, payment terms, default categories.
  4. Master data — products: templates first, then variants. Includes UoM, supplier links, accounting accounts per category.
  5. Master data — bills of materials (if manufacturing): components, routings, work centers.
  6. Open transactions: open sale orders, open purchase orders, open invoices, open shipments.
  7. Inventory: opening stock per product per location.
  8. Accounting balances: opening account balances as of cutover date.

Skip this order — try to import an open invoice before the customer and product exist — and the import fails on every row. Following the order produces clean imports.

External IDs are non-negotiable

Every record imported into Odoo should have an external ID (also called XML ID). The external ID is a stable reference that survives between runs. It's how you detect duplicates and update existing records on re-import.

Odoo's import format expects the external ID in a column named id or External ID. The convention: module_name.record_identifier.

# Example: products with external IDs
id,default_code,name,list_price,categ_id
__migration__.product_SKU001,SKU001,Red T-Shirt,29.99,__migration__.cat_apparel
__migration__.product_SKU002,SKU002,Blue T-Shirt,29.99,__migration__.cat_apparel
__migration__.product_SKU003,SKU003,Leather Belt,49.99,__migration__.cat_accessories

The __migration__ module prefix is a convention — choose anything stable. The benefit appears on re-import: Odoo finds the existing record by external ID and updates it rather than creating a duplicate. This is what makes migrations re-runnable.

Note. Foreign key references use external IDs too. The example above references __migration__.cat_apparel — Odoo resolves the category by external ID. This means categories must be imported before products that reference them.

Customer/partner migration

Partners (res.partner) are the foundation. Every order, invoice, and contact references a partner. Get partners right; everything downstream gets easier.

Required fields

  • name — display name (company or person name).
  • company_typeperson or company.
  • is_company — boolean, true for companies.

Address fields

  • street, street2, city, state_id, zip, country_id.
  • State and country are foreign keys; reference by external ID (e.g., base.state_us_5 for California).

Commercial fields

  • customer_rank — >0 means customer.
  • supplier_rank — >0 means supplier.
  • vat — VAT/tax ID. Odoo validates the format per country.
  • property_payment_term_id — payment terms (foreign key).
  • property_account_receivable_id / property_account_payable_id — accounting accounts.

Contact persons attached to companies

Use parent_id to attach a contact to its company. Both must exist; Odoo creates a hierarchy where the contact inherits company-level fields like address.

# Companies first
id,name,is_company,company_type,vat,country_id
__migration__.partner_acme,Acme Corp,True,company,US123456789,base.us

# Then contacts within companies
id,name,is_company,parent_id,email,function
__migration__.contact_jane_acme,Jane Doe,False,__migration__.partner_acme,[email protected],Procurement Lead

Product migration

Products are the second master data set. The complication: Odoo distinguishes templates from variants.

Templates and variants

  • product.template — the parent record ("T-Shirt"). Holds shared attributes.
  • product.product — the variant ("T-Shirt / Red / Medium"). Auto-generated when attributes are configured.

For products without variants, importing the template creates one auto-generated variant. For products with variants, the order is: template → attributes → values → variants attach automatically based on attribute combinations.

Categories and accounting

Categories carry accounting policy: income account, expense account, valuation account, valuation method. Configure categories in Odoo before importing products; the products inherit category defaults.

Suppliers per product

Supplier-product links live on a separate model: product.supplierinfo. Each link records: which supplier, supplier's product code, lead time, supplier price, validity dates. Import these after products and partners exist.

Tip. If your source system has "products" that are really kits (one SKU that ships as multiple physical items), model them in Odoo as kits via the BOM with type Kit. Don't try to flatten kits into single products — you'll lose the inventory tracking that kits enable.

Open transactions: orders, invoices, shipments

Open transactions — sales not yet shipped, purchase orders not yet received, invoices not yet paid — are migrated as open records, not historical ones. The destination state matters.

Open sale orders

Import as quotations (state draft) or sales orders (state sale) depending on whether they've been confirmed in the legacy system. Required fields: partner, lines (with products and quantities), prices, taxes.

# Sale order header
id,name,partner_id,date_order,state
__migration__.so_001,SO/2026/0001,__migration__.partner_acme,2026-04-15,sale

# Sale order lines (one row per line)
id,order_id,product_id,product_uom_qty,price_unit
__migration__.so_001_l1,__migration__.so_001,__migration__.product_SKU001,5,29.99
__migration__.so_001_l2,__migration__.so_001,__migration__.product_SKU003,2,49.99

Open invoices

Account moves (invoices) require the chart of accounts to be set up. Import unpaid invoices with state posted and an open balance. Payment registration happens in Odoo when the customer pays — even if the legacy system tracked partial payments, simplify by booking the residual amount as the open invoice and writing off the paid amount as opening balance.

Open shipments

If products have been picked but not shipped, model as a stock picking in assigned state. The picking will be validated when the actual shipment leaves on or after cutover. Don't try to migrate picking history before the cutover date — that's archival data.

Inventory migration

Inventory at cutover is migrated via physical inventory adjustments — Odoo's mechanism for declaring "as of this date, this product at this location has this quantity."

The cleanest approach

  1. Reset inventory in Odoo to zero (zero per product per location).
  2. Import the cutover snapshot as a single physical inventory.
  3. Validate the inventory; Odoo creates the stock moves automatically and posts to the inventory adjustment account.

Required fields per inventory line: product (external ID), location (external ID), quantity. Lots and serial numbers attach if the product is tracked.

id,location_id,product_id,inventory_quantity,reserved_quantity
__migration__.inv_SKU001_main,stock.stock_location_stock,__migration__.product_SKU001,150,0
__migration__.inv_SKU002_main,stock.stock_location_stock,__migration__.product_SKU002,80,0
__migration__.inv_SKU003_main,stock.stock_location_stock,__migration__.product_SKU003,42,0
Important. Verify the inventory snapshot is taken at cutover, not before. Any sales or receipts between snapshot time and cutover go-live time will be missing from the inventory in Odoo. The narrow window of "snapshot to switchover" should be minutes, not hours.

Accounting balances

Opening balances are migrated as journal entries dated the day before go-live. Each account that has a balance gets an entry; the offsetting account is the Opening Balance account (or equity account, depending on local accounting practice).

The structure

Create one journal entry per account, dated cutover_date - 1:

  • Debit: account with debit balance.
  • Credit: corresponding equity/opening balance account.
  • Reverse for accounts with credit balance.

Once all opening balance entries are posted, the trial balance in Odoo on cutover date should match the trial balance in the legacy system. This match is the proof that accounting migration is correct. If the totals don't match, do not go live — the discrepancy is unrecoverable later.

Open AR/AP

Open invoices migrated in the previous step contribute to AR/AP balances. Make sure the opening balance entries don't double-count: the AR balance comes from open invoices, not from a manual opening balance for the AR account.

Cutover strategy

The cutover is the moment the legacy system stops being authoritative and Odoo takes over. Two patterns:

Pattern 1: Big bang

One sharp cutover. Legacy system goes read-only at time T; Odoo goes live at time T+ε. All migration happens in the gap.

Strengths: Clean break. No dual-system operation.

Risks: If migration fails, rollback is messy. Requires a tight, well-rehearsed cutover plan.

Right for: Most retailers under 200 employees. The simplicity wins.

Pattern 2: Parallel run

Both systems operate for a period (typically 1–4 weeks). All transactions are entered in both. Reconciliation runs daily.

Strengths: Catches data issues with low risk. Operators stay productive even if Odoo has problems.

Risks: Double work for the team. Reconciliation overhead.

Right for: Larger operations or complex businesses where the cost of a failed cutover is greater than the cost of dual operation.

The cutover checklist (big-bang)

  1. Migration scripts pass validation against latest production data extract (run weekly, then daily, then twice on cutover day).
  2. Cutover day: legacy system goes read-only at T-0.
  3. Final extract from legacy system at T+0:00.
  4. Migration scripts run from T+0:00 to T+~3 hours (depending on volume).
  5. Validation: trial balance match, open transaction count match, inventory snapshot match.
  6. Sign-off from finance lead and operations lead.
  7. Odoo goes live at T+~4 hours.
  8. Legacy system stays available read-only for 90 days as a safety net.
Important. Schedule cutover on a low-traffic day — typically a Saturday morning for retail or end-of-month for B2B. Never on a major sales event. Never the day before a major sales event. The 8 hours saved by going live early are not worth the recovery cost if anything fails.

Common migration failures

1. Trying to migrate full transaction history

Legacy data goes back 10 years; the migration tries to recreate every transaction. The result: months of work, brittle data quality, and exotic edge cases that were valid in the legacy system but invalid in Odoo. Migrate forward-going state, not history. Archive history in the legacy system or a data warehouse.

2. Skipping validation

Imports run "successfully" because Odoo accepts the rows — but the rows have wrong data. Wrong tax codes, wrong currencies, wrong customer attributions. Validate every field against expectations before importing.

3. Non-idempotent imports

Re-running an import creates duplicates because external IDs aren't used. By cutover day, the database has 3x duplicate customers. External IDs on every import, every time.

4. Trial balance not reconciled

Going live without confirming the trial balance matches. Six months later, the accountant discovers €100,000 of opening differences and there is no clean way to find the source. Trial balance reconciliation is a go/no-go gate.

5. Cutover with no rollback plan

The plan assumes migration will succeed. When something fails at hour 6, there's no clear rollback. The rollback plan is part of the cutover plan.

Reference notes

Sources verified against Odoo 19.0 documentation and standards bodies. Use these to confirm anything before applying it to your environment.

Frequently Asked Questions

How long does an Odoo migration take?

For a typical retail or e-commerce migration: 4–12 weeks of preparation, 1 day for cutover, 2–4 weeks of post-cutover stabilization. Preparation time is dominated by data quality work — cleansing the legacy data, mapping fields, building and testing the migration scripts. The cutover itself is the smallest part of the project.

Should I use Odoo's import wizard or write custom migration scripts?

The import wizard is fine for one-time master data loads of moderate complexity. For migrations involving multiple related entities, validation logic, or hundreds of thousands of rows, custom scripts (Python with the Odoo XML-RPC client, or a one-shot Odoo module that runs in a server cron) give you more control. The break-even is roughly when validation logic becomes hard to express in CSV transformations.

Can I migrate from Odoo Community to Odoo Enterprise?

Yes — it's the same database. Odoo Enterprise is Community plus additional modules. Migration is more an installation activity than a data migration: install Enterprise modules, run any data transformation needed (mostly accounting localization upgrades), and verify. Most of this is handled by Odoo's standard upgrade path if you're on Odoo Online or Odoo.sh.

What happens to the legacy system after cutover?

Common practice: keep it running as read-only for 90–180 days. Users and accountants can still look up historical records. After the safety period, archive the database (PostgreSQL dump, MySQL dump, or whatever the source format is) and decommission the running system. The archive is preserved indefinitely for audit and tax purposes — typically 7–10 years per local regulation.

How do I migrate custom fields from a legacy system?

If the custom field maps to an existing Odoo field (different name, same purpose), map it during transformation. If the custom field has no Odoo equivalent, add it via Odoo Studio (point-and-click custom field) or a small custom module. Avoid leaving custom data behind in the legacy system — that's where unrecoverable data loss happens.

What about historical reports? Can users still pull last year's sales report?

Two options. Option A: migrate historical sales as posted journal entries (no inventory or transactional integrity, just accounting). Reports work, but the data is opaque. Option B: keep the legacy system for historical reports, build new reports in Odoo for forward-going periods. Most retailers choose B — historical reports are pulled rarely; it's not worth complicating the migration.

Can I migrate while my business is running, with minimal downtime?

Yes — with a parallel run pattern. Both systems operate for a period; data migrates incrementally; cutover happens once Odoo is verified. The cost is operational complexity (entering each transaction in two systems for the parallel period) and reconciliation work. For businesses that genuinely cannot pause for a few hours of cutover, it's the right pattern. For most retailers, a Saturday morning big-bang cutover is simpler and has lower total cost.

Florinel Chis — commerce engineer leading Odoo and Magento implementations for retail and e-commerce. About Magendoo. Verified against Odoo 19.0
Founder-led Senior consulting, no agency layers
Magento + Odoo Specialist focus on commerce systems
EU Based in Europe, Serving Europe
OSS Open Source Contributor
Get a Proposal • 24h response Call