ERP migration guide

The Definitive Guide to Migrating to Odoo ERP

Odoo ERP is a module-based, open-source business suite whose migration path rewards teams that pre-stage the chart of accounts, model inventory locations and BOMs against existing modules, and respect platform throughput limits before the first opening balance is posted.

23 min read 9 sections Updated May 27, 2026
Odoo ERP
Chart of Accounts
Customers
Vendors
Items
Transactions
Inventory

Inside this guide

What you'll learn, section by section

  1. 01

    Why teams migrate to Odoo ERP

    The four shapes an Odoo ERP migration takes, and what makes the platform easier — or harder — than the category average.

  2. 02

    The Odoo ERP data model you need to map into

    Models, ondelete semantics, and the external IDs you'll wire on every relational column — the destination schema decoded.

  3. 03

    Pre-migration prep — the work before you touch Odoo ERP

    What must be true on the source, the destination, and across the team before the first row hits the import wizard.

  4. 04

    Import mechanisms: UI wizard, base_import, and scripted loads

    Multiple paths in, each with different limits and shapes. Picking the wrong one is how mid-migrations stall at the throughput ceiling.

  5. 05

    Mapping your data into Odoo ERP

    The longest section — because field mapping is where almost every ERP migration that fails actually breaks.

  6. 06

    The pitfalls that derail Odoo ERP migrations

    Ten specific failure modes — ranked by impact, each tied to the exact Odoo mechanism that breaks.

  7. 07

    Validation and cutover

    What to verify after the import job, in what order — and how to fail safely when something is wrong.

  8. 08

    Migration partners and tools

    Official partners, OCA modules, iPaaS connectors, specialist migration shops — what each is good for and how to choose.

  9. 09

    Frequently asked questions

    The eight questions every Odoo ERP migration team works through before they sign the scope.

Section 01

Why teams migrate to Odoo ERP

The four shapes an Odoo ERP migration takes, and what makes the platform easier — or harder — than the category average.

Odoo S.A. was founded by Fabien Pinckaers in 2005 as TinyERP, rebranded to OpenERP in 2009, then to Odoo in 2014, and is headquartered in Ramillies-Brabant-Wallon, Belgium 1. Odoo ERP is the integrated finance, inventory, manufacturing and procurement layer of a suite of around 80 official modules — Accounting, Inventory, Manufacturing, Purchase, Sales, Project, HR — all sharing the same PostgreSQL database and ORM.

The typical Odoo ERP customer is a small-to-mid-market manufacturer, distributor or services business — 25 to 500 users — that wants general ledger plus inventory plus production plus AP/AR under one schema, and is willing to absorb the open-source learning curve for the integration depth 2. Compared with SAP Business One or Dynamics 365 Business Central, Odoo positions on per-user cost; compared with NetSuite, on module breadth and a unified data model 3.

The shapes of migration that actually land on Odoo ERP tend to fall into four patterns. First, suite consolidation projects: a company replacing a stack of QuickBooks plus a separate WMS plus a spreadsheet-based MRP with the Odoo all-in-one suite. Second, vendor-cost exits from SAP, NetSuite or Oracle where total cost of ownership at 50–400 users no longer justifies the enterprise contract 5.

Third, legacy ERP replacements — older OpenERP/TinyERP installs, Sage 50, JD Edwards on-prem, JTL-Wawi, or country-specific country-by-country systems — where the source schema is loose and the project is really a re-architecture against account.move and stock.move 6. Fourth, Odoo-to-Odoo version migrations, where a team running Community 15 jumps to 18 or 19, or moves from Odoo.sh Enterprise back to self-hosted Community, with OpenUpgrade carrying the schema 7.

Each shape has a different difficulty profile: a QuickBooks-to-Odoo move usually has clean GL parity but messy chart-of-accounts numbering, while a SAP-to-Odoo migration carries rich automation that does not move across at all.

What makes migrating *to* Odoo ERP easier than the category average is the import tool itself — it accepts CSV and Excel files, auto-detects column types, supports relational fields via XML external IDs, and ships built into every model's list view through Actions → Import records 8. Pre-built templates exist for Contacts, Products, Chart of Accounts, Journal Entries and Bills of Materials.

What makes it harder than the average is the Community-vs-Enterprise gap — full Accounting (audit trail, follow-up letters, online bill scanning), Studio, Documents, Marketing Automation and several connector modules are Enterprise-only, and silently disappear if you restore an Enterprise database into a Community container 7. The second hard edge is the annual major-version cadence: every 12 months a breaking release ships, which means migration is not a one-time event but a recurring workstream every team plans around 11.

Studio customisations, Automated Actions, custom Python modules and country-specific localisations do not move with a CSV import — they are rebuilt as modules and configured in the destination. Teams that scope for that work up front finish on time; teams that assume parity do not.

Studio customisations, localisations and Automated Actions do not move with a CSV import — they are rebuilt in the destination.

Section 02

The Odoo ERP data model you need to map into

Models, ondelete semantics, and the external IDs you'll wire on every relational column — the destination schema decoded.

Odoo platform Contacts Companies Deals Tickets Tasks Notes
Standard objects orbit the platform; every association can be many-to-many with optional labels.

Odoo's ERP is built around a small set of Python models, each declared as a class extending models.Model with attributes that the ORM persists into PostgreSQL 12. Finance, inventory and manufacturing sit on top of the same shared res.partner master record and account.account chart of accounts used by every other Odoo app.

Before you can map a field on the source side, you need to know exactly which destination model the row belongs on, what fields it requires, and which value will serve as its external ID for upsert. The table below summarises the models you will touch in an Odoo ERP migration 13.

Object Stores Required on import Tier
res.partner Master record for customers, vendors and employees name; is_company; customer_rank / supplier_rank All editions
account.account Chart of Accounts — every GL account code; name; account_type (income, expense, asset, liability, equity) Full Accounting in Enterprise; basic Invoicing in Community
account.move / account.move.line Journal entries — invoices, bills, opening balances, all GL postings journal_id; date; line_ids (debit/credit balanced) All editions
account.journal Journals — Sales, Purchase, Bank, Cash, Miscellaneous name; type; code All editions
product.template / product.product Product master and per-variant SKUs name; type (consu / service / product); list_price All editions
uom.uom / uom.category Units of Measure (kg, m, box of 6) and their conversion categories name; category_id; factor Inventory module
stock.location / stock.warehouse Physical and logical inventory locations name; usage (internal, customer, supplier, inventory) Inventory module
stock.quant / stock.move / stock.lot On-hand quantities, stock movements, and lot/serial numbers product_id; location_id; quantity Inventory module
mrp.bom / mrp.bom.line Bills of Materials and their components product_tmpl_id; product_qty; bom_line_ids Manufacturing module
purchase.order / sale.order Purchase and sales orders with their order lines partner_id; date_order; order_line Purchase / Sales modules
res.currency / res.currency.rate Currencies and dated exchange rates name (ISO code); rate; date All editions
res.company Legal entity — drives multi-company isolation name; currency_id; country_id All editions; Enterprise required for inter-company rules

res.partner is the unifying record — a single row represents a customer, a vendor, an employee, or a contact at any of those, distinguished by the customer_rank and supplier_rank integer counters and linked through parent_id for company-and-contact hierarchies 14.

Odoo does not have a single canonical natural-key field on most models. Instead, the import tool uses an XML external ID — a string of the form __import__.123 or your_module.account_4000 — stored in the id column on the import spreadsheet. The external ID is the upsert key: re-importing the same row with the same external ID updates; a new external ID creates.

If you do not supply external IDs, Odoo generates them automatically and you lose the ability to deterministically re-import. For native dedup on res.partner, install the Data Cleaning app and configure dedup rules on email, phone, or VAT number. Custom field types determine validation and storage; the catalogue below covers what you can model and the limits you need to plan around 12.

Field type Limits Notes
Char (single-line text) Default size=64, configurable Maps to PostgreSQL varchar; trim before import
Text (multi-line text) Unlimited (PostgreSQL TEXT) Used for descriptions, internal notes
Integer / Float INT4 / NUMERIC in PostgreSQL Float defaults to digits=(16,2); override per field for GL precision
Monetary Tied to res.currency precision Use for amounts; system handles rounding by currency decimals
Date / Datetime YYYY-MM-DD / YYYY-MM-DD HH:MM:SS in UTC Datetime stored UTC; ISO-8601 strongly recommended
Selection (picklist) Defined as Python tuple list Internal key vs label; import expects the key 12
Many2one (foreign key) Single related record Resolves by name OR external ID OR database ID — pick one and be consistent 17
One2many / Many2many Inverse / junction relations Many2many uses comma-separated names on import 18
Binary (attachments) 64 MiB default on Odoo.sh; 128 MiB on self-hosted Stored as ir.attachment records 19
Custom fields via Studio 21 field-type variants, Enterprise-only UI Community: edit via Developer Mode → Settings → Technical → Fields 20

Relationships in Odoo are modelled by relational fields with explicit ondelete semantics: cascade deletes children when the parent is deleted, restrict blocks the parent delete, set null clears the foreign key 21. This is configured in fields.py per field, not at the database level you can flip from the UI — and it drives several of the pitfalls in Section 6.

Custom Objects in Odoo are not a separate concept — they are simply new Python models, declared in a custom module's models/ folder, or created via Studio (Enterprise) which writes the same metadata into ir.model and ir.model.fields 20. Either way, the model must exist in the database before records of that type can be imported, and the module must be installed before the model is reachable.

Section 03

Pre-migration prep — the work before you touch Odoo ERP

What must be true on the source, the destination, and across the team before the first row hits the import wizard.

The single best predictor of a clean Odoo ERP migration is how much work you do on the source side before the first import button is pressed. Odoo's own implementation partners warn that data preparation — not the upload itself — is where most projects lose weeks, and the fix is almost always pre-processing rather than post-cleanup.

The single best predictor of a clean migration is how much work you do before the first import button is pressed.

Treat the source export as raw material that needs to be shaped to Odoo's expected formats — dates rewritten to YYYY-MM-DD (ISO-8601), Selection values converted to their internal keys, Many2one relations resolved to either a name string or an external ID, currencies resolved to ISO codes that exist in res.currency, GL accounts mapped to the destination chart of accounts, and every row stamped with a stable id column.

Source-side prep

  • Audit and dedup the customer and vendor master in the source system before export. Odoo's Data Cleaning app handles post-import dedup on res.partner matching on email, phone and VAT, but it is much cheaper to dedup on the source than to merge after.
  • Reconcile the source GL to its trial balance before extracting opening balances — every account that does not tie out on the source becomes a manual line item on the Odoo opening journal entry.
  • Normalise dates to YYYY-MM-DD or YYYY-MM-DD HH:MM:SS in UTC. Odoo's importer auto-detects most formats but day/month inversions like 01-03-2016 are ambiguous; use ISO-8601 to remove the guesswork.
  • Convert Selection field labels to internal keys — for account types, journal types, product types, every custom dropdown — Odoo expects the Python key (e.g. asset_receivable, liability_payable, consu, product), not the display label 12.
  • Stamp a stable external ID in an id column on every export row — a UUID, the source platform's primary key, or a namespaced string like legacy.account_4000. This is what makes re-runs and reconciliation deterministic.
  • Decide what is in scope for historical transactions. Open AP/AR invoices and the opening trial balance must move; closed prior-year journal entries usually stay in the source as a read-only archive.

Destination-side prep

  • Install the country localisation under Apps → search l10n_ (for example l10n_us, l10n_fr, l10n_de) on a fresh database *before* any other Accounting setup — the localisation seeds the chart of accounts, taxes, and fiscal positions, and cannot be cleanly applied after journal entries exist 27.
  • Create a staging database on Odoo.sh (staging branches are first-class) or duplicate the database on Odoo Online or on-prem to dry-run the import end-to-end. Restore a sanitised copy of production into staging weekly during the project.
  • Provision users first under Settings → Users & Companies → Users, assigning each to Accounting, Inventory, Manufacturing and Purchase groups. Owner assignment fails silently if the referenced user does not exist.
  • Pre-create every custom field on the right model via Studio (Enterprise) or Developer Mode → Settings → Technical → Fields (Community). Selection fields must have their full key list defined before any value is imported 20.
  • Build the chart of accounts, journals, fiscal years, and tax codes in Accounting → Configuration before importing any journal entries — entries targeting a non-existent account or a closed period are rejected.
  • Install required modules first — Accounting, Inventory, Manufacturing, Purchase, Sales, plus any OCA modules (OCA Audit Log, fs_attachment, OpenUpgrade) you intend to depend on. Trying to import to a model whose module is not installed throws Model not found.

People prep

Cutover only works if humans cooperate. Lock down a source-system freeze window — typically 24 to 72 hours for finance, longer for inventory if you are recounting — and communicate it to every department that touches the ERP. Train AP clerks on the Odoo vendor bill workflow, warehouse staff on the kanban barcode flow, and accountants on the journal-entry and reconciliation widgets before go-live, not after.

A typical 50-user professional-services Odoo migration runs four to eight weeks of elapsed time end-to-end; a 200-user multi-company manufacturing migration with Studio rebuilds, multi-currency consolidation and historical inventory loads runs three to six months 30. Build the human runway accordingly.

Section 04

Import mechanisms: UI wizard, base_import, and scripted loads

Multiple paths in, each with different limits and shapes. Picking the wrong one is how mid-migrations stall at the throughput ceiling.

Odoo exposes several load paths and the right one depends on dataset size, model mix, and whether you need to re-run idempotently. The UI Import wizard (powered by the base_import module) covers most one-shot migrations under a few hundred thousand records. Scripted loads via the Odoo ORM handle programmatic, repeatable and very large loads. Third-party tools sit on top of both and add staging, transformation and dedup layers.

UI Import wizard (base_import)

The native import lives on every model's list view: open the model (Contacts, Accounting → Chart of Accounts, Inventory → Products, etc.), click the gear or Actions icon, then Import records → Upload File 8. The wizard accepts CSV and Excel (XLSX) files, auto-detects column-to-field mapping by name, exposes a Show fields of relation fields (advanced) toggle to see related model fields, and offers a Test mode that validates the file without writing rows.

There is no hard daily row cap in base_import, but practitioner reports suggest CSV-via-UI is reliable up to roughly 10,000 records per file and starts to hit worker timeouts above 50,000 on Odoo.sh and Online. The right call: UI for one-shot loads under 100k records on standard models, or any time you want a visual mapping review and a Test pass before commit.

For Excel files, save as .xlsx (not .xls) — older formats are less reliable. Re-imports against the same external ID in the id column update existing rows; new external IDs create. Warning: imports are permanent and cannot be undone from the UI — use the created on or last modified filter to locate import-batch rows for archival 17.

Scripted loads through the Odoo ORM

Odoo's ORM exposes the same methods as the UI — create, write, search_read, and the bulk-friendly load — and can be driven by scripts running against an Odoo instance. The load() method is what base_import calls internally; it accepts a list of field names and a list of value rows and applies the same external-ID upsert semantics as the UI wizard. For bulk loads, batch records into chunks of 100 to 500 per create or load call; smaller batches add round-trip overhead, larger batches risk timeouts on Odoo.sh. Model.create() accepts a list of dictionaries — bulk-insert in a single call rather than a loop 33.

Choose the scripted path when the load is over 100k records, when custom models are involved, when the migration needs to be repeatable and audit-logged from your side, or when you are syncing from an external warehouse. Wrap calls in a back-off loop on throttling and connection-reset errors.

Third-party staging tools and OpenUpgrade

Tools like Fivetran, Airbyte and Stacksync ship Odoo connectors that sit between the source and the destination. They are commonly used in two ways: (a) reverse-ETL where the source database is loaded into a warehouse, transformed in SQL, then synced into Odoo; and (b) staging-and-validate, where the tool generates the import CSV, runs type coercion, and feeds the result to the wizard.

For Odoo-version migrations specifically (Community 15 → 18, etc.), the Odoo Community Association's OpenUpgrade project provides the only working open-source upgrade scripts for Community editions 7. For Odoo Enterprise upgrades, Odoo S.A.'s paid upgrade service handles the schema transformation but is not available to Community users — and the annual major release cadence makes this a recurring cost line rather than a one-time event 11.

Rule

Under 10,000 records on standard models → UI base_import wizard. 10,000–100,000 → UI wizard in batched files. Over 100,000, any custom models, opening-balance journal entries, or any re-runnable load → scripted ORM load() calls in chunks with a back-off loop.

Section 05

Mapping your data into Odoo ERP

The longest section — because field mapping is where almost every ERP migration that fails actually breaks.

SOURCE ODOO ERP FirstName, LastName firstname, lastname AccountName company AnnualRevenue annualrevenue Owner.Email hubspot_owner_id CreatedDate createdate
Field-mapping flow — every source field resolves to a destination property or an explicit drop.

Mapping is where every ERP migration earns its scars. The schema decisions you make in your mapping spreadsheet determine whether the trial balance ties on day one, whether inventory valuation matches on day five, and whether the closing process works at the first month-end after go-live.

Work model by model, top to bottom of the dependency order: res.company and res.currency first, then res.users, then account.account (chart of accounts) and account.journal, then customers and vendors on res.partner, then product.template with uom.uom, then stock.location and on-hand inventory, then mrp.bom, then open AP/AR via account.move, and finally the opening trial balance.

Chart of accounts (account.account)

Install the country localisation module (e.g. l10n_us, l10n_fr, l10n_de) before any chart-of-accounts work — the localisation seeds the legally required CoA structure, default taxes and fiscal positions 27. Then map the source CoA to the localised CoA: most projects keep the localised account codes and only add custom accounts for operational tracking (departments, projects, cost centres) that the source had as analytic dimensions.

Common source → account.account mapping

Source Destination
  • GL account number
    code

    Renumber to fit localised CoA ranges; document the cross-walk

  • account name
    name

    Required; keep source name as legacy_name Char if useful

  • account type / category
    account_type

    Selection key: asset_current, asset_receivable, liability_current, liability_payable, income, expense, equity 27

  • reconcilable flag
    reconcile

    Set True on AR/AP control accounts; required for invoice matching

  • currency restriction
    currency_id

    Only set if account is restricted to a single non-company currency

GL opening balances (account.move)

Opening balances post as a single Miscellaneous journal entry on account.move dated the day before go-live (e.g. 2026-01-01 entry on a 2026-02-01 cutover), with one account.move.line per account, debits equal to credits 35. The exception is AR and AP control accounts — those balances post as individual open invoice and bill records (next subsection), not as a single line, so that future receipts and payments can match by document.

Recommended sequence on the opening entry: assets first (cash, inventory control, fixed assets net of accumulated depreciation), then liabilities (accrued expenses, taxes payable, loans), then equity (retained earnings as the balancing account). The journal must balance to the penny — Odoo rejects unbalanced entries with The total of the debits is not equal to the total of the credits.

Open AP / AR invoices (account.move with move_type)

Open customer invoices import as account.move records with move_type='out_invoice' and state='posted'; open vendor bills use move_type='in_invoice'. Each invoice carries its own due date, partner, lines, tax and amount-residual — and crucially, its own original invoice date so that aging reports work correctly from day one.

Build one row per invoice header plus one row per line item (One2many on invoice_line_ids), with the invoice's external ID repeating on each line. After import, run the Accounting → Reporting → Partner Ledger report and reconcile against the source AR/AP aging — any partner whose balance does not match within rounding tolerance flags a mapping defect to fix before go-live.

Inventory: products, UoM, locations, on-hand quantities

Inventory mapping touches four models in sequence. First, uom.uom and uom.category — every UoM you reference must already exist, and conversions only work between UoMs in the same category (Units, Weight, Volume, Length, Time) 36. If you sell in meters and buy in centimetres, both must be in the Length category with the correct factor.

Second, product.template and product.product — the template is the catalogue item, the product.product is the variant (size, colour, configuration). Required fields: name, type (Selection key: consu, service, or product for storable), list_price, uom_id, uom_po_id, and categ_id (product category). Set tracking to lot or serial if the SKU needs lot/serial tracking — this must be decided *before* the first stock move exists for that product, because Odoo blocks the change once movements have posted 37.

Third, stock.warehouse and stock.location — warehouses contain a tree of locations (Stock, Stock/Shelf A, Stock/Quality Control, Customers, Vendors, Inventory adjustment). Each location has a usage (Selection key: internal, customer, supplier, inventory, production, transit).

Fourth, on-hand inventory — the recommended path is Inventory → Operations → Physical Inventory which creates an Inventory Adjustment that posts as a stock.move from the virtual Inventory adjustment location into the real internal location, with the per-product quantity and lot/serial number where applicable. The corresponding GL entry hits the inventory control account on the opening journal entry — do not double-count by posting both the stock.move and a manual GL line for the same value.

Bills of materials (mrp.bom)

BOMs import as mrp.bom headers (one row per assembly) plus mrp.bom.line children (one row per component). The header carries product_tmpl_id, product_qty, code, and type (Selection key: normal, phantom for kit-bill, subcontract). The lines carry product_id, product_qty, and product_uom_id 39.

BOM imports are bottom-up: every component product must exist before the BOM that references it, and multi-level BOMs need the sub-assembly's BOM imported before the top-level BOM that includes the sub-assembly. Practitioners report BOM creation is one of the heavier operations because each BOM needs the header create, then one create per line — under Odoo Online's tighter throttling, even a few thousand BOMs becomes hours 22.

Multi-currency and historical exchange rates

Multi-currency is enabled in Accounting → Configuration → Settings → Currencies → Multi-Currencies and configured per-company through res.company.currency_id. For historical transactions in foreign currency, you import the original amount and original date in amount_currency and date, and Odoo posts the company-currency equivalent using the rate from res.currency.rate for that date — so the rate table must be populated for every historical date that has a foreign-currency transaction.

Without dated rates, Odoo falls back to the current rate and posts the wrong company-currency value 41. Build the rate import as one row per (currency, date, rate) triple and import it before any multi-currency invoices or journal entries. For multi-company setups, each res.company can have its own functional currency, and inter-company transactions revalue automatically.

Fiscal year and period close

Fiscal year is configured in Accounting → Configuration → Settings → Fiscal Periods (last day of fiscal year, lock dates). Set the Journal Entries Lock Date to the day before your opening-balance date so that nobody (including the import job) can post prior-period entries by accident 27. After opening balances and open invoices are loaded and reconciled, advance the lock date to the day before the new period — this is how Odoo enforces period close.

Custom-field mapping strategy

Resist the urge to map every source custom field one-to-one. Migrate only the custom fields used by an active process, automated action, or report in the last 12 months. Excess fields slow down list views and make the model harder to upgrade between Odoo versions because every custom field has to round-trip through OpenUpgrade or the paid upgrade service 7.

For Selection fields whose source values do not match the destination, either: (1) extend the destination Selection with the missing internal keys via Studio or fields.py, (2) collapse adjacent values during transform, or (3) introduce a parallel legacy_value Char field that holds the source value verbatim. Computed fields do not import — Odoo recomputes them on save — so any source formula must be rebuilt as a computed field on the model or replicated via Server Actions.

Files, attachments, and audit trail

Attachments in Odoo live on the ir.attachment model and link to any record via res_model and res_id. Default per-attachment cap is 64 MiB on Odoo.sh and Odoo Online (the public reverse proxy enforces this) and 128 MiB on self-hosted instances that override server config 19. Supported image formats include JPEG, PNG, GIF, BMP, TIFF, SVG, ICO, WEBP, PSD and EPS 42.

Bulk attachment import is not part of base_import — the supported path is either (a) the Documents app (Enterprise) which exposes a drag-and-drop bulk uploader, (b) the fs_attachment OCA module which moves the filestore to S3 or an external filesystem 43, or (c) scripted upload through the ORM where you load ir.attachment records with datas, name, res_model and res_id set.

Odoo records create_date, create_uid, write_date and write_uid automatically and does not preserve the source record's original creation date during importcreate_date stamps to upload time 44. If you need the original audit trail, create legacy_create_date (Datetime) and legacy_create_uid (Many2one to res.users) custom fields per model and populate from the source export. Field-change auditing is provided by the Enterprise Accounting Audit Trail feature, and the OCA Audit Log module fills the same role on Community 45.

Section 06

The pitfalls that derail Odoo ERP migrations

Ten specific failure modes — ranked by impact, each tied to the exact Odoo mechanism that breaks.

High impact

Localisation installed after journal entries already exist

The l10n_* localisation modules seed the chart of accounts, default taxes and fiscal positions. They are designed to be installed on a fresh database; once journal entries reference accounts, applying or switching a localisation throws constraint errors and partially-loaded CoA rows that the UI cannot cleanly delete 27. The recommended path is: spin up a fresh database, install the country localisation first, then run every other migration step. Switching localisation mid-project usually means restoring from backup. 27

High impact

Enterprise modules silently disabled on a Community restore

Restoring an Odoo.sh Enterprise database into a self-hosted Community container marks Enterprise-only modules — Accounting (full), Documents, Studio, Marketing Automation, Audit Trail — as 'installed' but 'not installable, skipped'. The UI crashes with errors like View types not defined map found in act_window and Studio customisations vanish from form views 7. Before you switch hosting tiers, list every installed module via Settings → Apps → Installed and confirm each has a Community equivalent or a replacement plan. 7

High impact

Opening journal entry does not balance to the penny

Odoo rejects any account.move whose total debits do not equal total credits with The total of the debits is not equal to the total of the credits and refuses to post. Rounding differences from a foreign-currency source ledger, fractional cents in inventory valuation, or accumulated retained-earnings calculation off by 0.01 are the usual culprits. The fix is a single 'Rounding Adjustment' line posted to a designated rounding account on the opening entry — do not start chasing the penny across operational accounts. 35

High impact

Selection field label vs internal key mismatch

Odoo Selection fields — account.account.account_type, product.template.type, account.move.move_type, every custom dropdown — store an internal Python key distinct from the UI label. The label is Receivable; the key is asset_receivable. The import wizard rejects label-only payloads with a vague Unsupported format character or silently coerces to a default. Build your transform layer against the model's selection tuple in code (Developer Mode → Settings → Technical → Fields), never against what an admin sees in the UI 12. 12

High impact

Throughput throttling on Odoo Online slows large loads

Practitioners testing importers on Odoo 17 report the Odoo Online terms of use enforce tight throughput throttling 22. Combined with Odoo's standard 'creating more than 2-3 sales orders per second per worker is difficult' guidance even on Enterprise, a BOM-and-inventory load can take days instead of hours. Drop concurrency to a single worker on Odoo Online and rely on chunked load() calls of 100–500 rows; on Odoo.sh and self-hosted, scale parallelism against your server's worker count. 22

High impact

Product tracking changed after the first stock move exists

Setting product.product.tracking to lot or serial is irreversible once stock has moved against that product. If you import inventory without tracking enabled and later need to enable lot tracking, Odoo blocks the change with You can only change the tracking of a product on a product without quantities on hand — and your remediation is to liquidate all stock to a virtual location, change tracking, then re-import with lots 37. Decide tracking model per product before the first stock.move posts. 37

High impact

Multi-currency without dated exchange rates loads wrong company-currency values

When a multi-currency invoice imports with currency_id set to a foreign currency, Odoo uses the rate on res.currency.rate for the invoice's date to compute the company-currency equivalent. If the rate for that date does not exist, Odoo falls back to the most recent available rate and posts a materially wrong number — and the field-level error log gives no warning 41. Populate res.currency.rate for every distinct invoice date in the foreign-currency dataset *before* importing the invoices. 41

Medium impact

create_date cannot be overridden on import

Odoo does not preserve the source record's creation date during import — create_date, create_uid, write_date and write_uid are ORM-managed and stamp to upload time and the import user 44. Teams discover this on day two when reports filtered by *Created Date* return everything stamped to import day. The mitigation is to create legacy_create_date and legacy_write_date custom Datetime fields on every imported model, populate them from the source export, and rewrite historical-period reports to use the legacy fields. 44

Medium impact

Date format auto-detect inverts day and month

Odoo's importer auto-detects date columns but warns that day-month inversions like 01-03-2016 are ambiguous and may be guessed wrong. Worse, error messages from XLSX imports surface as Column activity_ids/date_deadline contains incorrect values. Error in line 1: unconverted data remains: 2019-12-16 with no field-level hint about what went wrong 46. Always set the Date Format explicitly in the wizard's Formatting Options to ISO-8601 (YYYY-MM-DD), or convert to ISO before export.

Medium impact

ondelete=restrict blocks parent deletion across the dependency chain

Relational fields in Odoo specify ondelete='cascade', 'restrict' or 'set null' per field 21. If you import test data into a sandbox, then try to delete a parent record (a res.partner referenced by an account.move, a product.template referenced by a stock.move), Odoo blocks the delete with The operation cannot be completed: Another model is using the record you are trying to delete 47. Plan archival, not deletion, for parent records — use the active=False boolean to soft-delete. 21

Low impact

Annual major-version cadence forces a recurring migration workstream

Odoo ships a breaking major release every 12 months. Each release ages out community modules, deprecates ORM patterns and may shift schema in localisations 11. A migration in 2026 to Odoo 19 will face an OpenUpgrade or paid-upgrade event in 2027 to reach Odoo 20, in 2028 for 21, and so on. Treat migration as an ongoing capability rather than a one-time project; lock customisations into named modules with manifest version pins, and budget upgrade cycles into the operating cost from year one. 11

Low impact

Data residency is fixed at signup on Odoo Online and Odoo.sh

Odoo.sh has seven Google Cloud regions — Iowa, Toronto, Belgium, Dammam, Mumbai, Singapore, Sydney — and Odoo Online assigns customers based on region at signup. There is no bring-your-own-cloud option, no African data centre, and changing region after signup requires a support-managed migration that schedules in weeks. If your project requires EU residency under GDPR or India DPDP residency and the account was created in the wrong region, plan the regional move *before* the data migration, or move records twice.

Section 07

Validation and cutover

What to verify after the import job, in what order — and how to fail safely when something is wrong.

1 Read-only Source goes write-frozen 2 Final delta Export incremental changes 3 Import Load into Odoo 4 Validate Reconcile + spot-check 5 Cut over Users on new system
Cutover sequencing — five gated phases between source read-only and full user access.

Validation is the bridge between the import finishing and users being allowed in. Odoo implementation partners recommend a three-stage validation: a test load of 10 percent of records with stakeholder spot-checks, the full load with real-time monitoring of GL balance and stock counts, and a 30-day post-migration data-quality audit. The most reliable signal is having finance, AP, AR and warehouse leads verify their own records against the source.

Build a reconciliation queries spreadsheet that compares source and destination on each of these counts. Anything outside a 0.5 percent variance — or any non-zero variance on the trial balance — gets investigated before users get login access.

  • Trial balance match — run Accounting → Reporting → Trial Balance on Odoo dated the opening-balance date and reconcile every account to the source trial balance. Any non-zero variance is a defect, not a rounding issue.
  • AR aging by partner vs source — run Accounting → Reporting → Partner Ledger filtered to receivable accounts and compare partner-by-partner totals against the source AR aging report.
  • AP aging by vendor vs source — same as AR, against the source AP aging.
  • Stock on hand by warehouse and location vs the source physical count — run Inventory → Reporting → Inventory Valuation and reconcile against the source warehouse management report.
  • Inventory valuation vs the source GL inventory control account — these must tie within rounding; any larger variance indicates a unit-of-measure conversion error or a cost-method mismatch (FIFO/Average/Standard).
  • Open purchase and sales orders vs source — count distinct purchase.order and sale.order in draft or purchase/sale state and verify against the source open-orders list.
  • BOM count and component countGROUP BY product_tmpl_id on mrp.bom and compare against the source manufacturing master.
  • External-ID uniqueness — query ir.model.data for duplicate (module, name) combinations on your migration namespace; any duplicates indicate the dedup transform missed cases.
  • Multi-company isolation — if multi-company is in scope, query each res.company and confirm account.move rows are scoped to the right company_id.

On top of reconciliation, run a manual spot-check protocol: pick 30 random records across models — five open invoices, five vendor bills, five products with inventory, five BOMs, five sales orders, five journal entries — and verify each field against the source UI. If a non-trivial discrepancy shows up in three or more of the 30, halt the load, fix the root cause, and re-import the affected rows by external ID.

Odoo does not ship a native bulk-undo for imports — the documentation is explicit that imports are permanent 17. The closest things are the OCA Audit Log module (when configured with the right rules in advance) 45, the Enterprise Accounting Audit Trail for journal entries, and Odoo.sh staging branches which let you fork production and roll back at the database level. None is a one-click reversal; all depend on having set them up *before* the import.

The real rollback strategy remains: take a full PostgreSQL backup via /web/database/manager (pg_dump -Fc plus the filestore directory) before the import starts, stamp every imported row with a custom import_batch_id Char field, and if catastrophe strikes, archive-by-batch and re-import from the cleaned source.

Cutover sequencing: (1) source goes read-only and the finance, AP, AR and warehouse teams are notified; (2) physical inventory count completes and loads as opening on-hand; (3) final delta export captures everything that changed during the test-import window; (4) delta imports with the same external-ID namespace; (5) trial-balance and AR/AP reconciliation runs; (6) lock dates advance to the day before go-live; (7) users get login access plus a 48-hour hyper-care window; (8) source decommission schedules 90 to 180 days out.

Section 08

Migration partners and tools

Official partners, OCA modules, iPaaS connectors, specialist migration shops — what each is good for and how to choose.

Odoo S.A. runs a tiered partner programme — Gold, Silver and Ready — based on certifications, customer count and recurring Enterprise revenue, and lists 5,000+ partners across 120+ countries on its public directory 52. For ERP migrations specifically, partners with explicit SAP-to-Odoo, NetSuite-to-Odoo, Sage-to-Odoo or QuickBooks-to-Odoo practices tend to ship cleaner than generalist shops30. The Odoo Community Association (OCA) maintains modules — Audit Log, OpenUpgrade, fs_attachment — that fill gaps left by Enterprise 745.

Cybrosys Technologies, Open Technology Solutions, Synavos, Aspire Softserv, Silent Infotech, Iqra Technology, Softeko, Braincuber, Steersman and ECOSIRE are commonly named in the migration corner of the market and each offers fixed-scope or hourly-rate migration packages alongside ongoing managed services30. Odoo S.A. also runs a paid upgrade service for Enterprise customers, which handles version-to-version schema migration but is not available to Community users — for Community, OpenUpgrade is the working alternative 7.

On the ETL and iPaaS side, Fivetran, Airbyte and Stacksync ship Odoo connectors, and several Odoo Apps Store extensions (Import Bridge by Axis, base_import_async by OCA) add async-job patterns on top of the wizard. Their role in an ERP migration is rarely the migration itself — it is the staging layer, the transformation layer that resolves Selection values and Many2one relations, and the ongoing-sync layer post-cutover.

Managed-migration cost ranges vary widely with user count and module scope. Practitioner pricing for Odoo ERP migrations runs roughly $3,000–$10,000 for small businesses (under 10 users, single-company, standard modules), $20,000–$50,000 for medium businesses (11–50 users, multiple modules with moderate customisation), and $50,000–$100,000+ for large enterprises (50+ users, multi-company, heavy customisation, multiple integrations) 30.

Hourly rates for Odoo professional services range $10–$150+ per hour depending on region and partner expertise. The upper end of the curve is driven by user count, the number of modules in scope (Accounting, Inventory, Manufacturing, Purchase, Sales, HR, Project — each adds elapsed time), the depth of historical data, multi-currency or multi-company complexity, and the number of third-party integrations that need rebuilding rather than re-pointed.

For teams that want to outsource the migration end-to-end, FlitStack specialises in Odoo ERP migrations and handles the localisation install, chart-of-accounts cross-walk, opening-balance journal preparation, inventory and BOM load, multi-currency rate population, and validation work described in Sections 5 and 7 of this guide. Pricing is fixed-fee, based on user count, module scope and source platform, with separate line items for multi-company, historical-data depth and custom modules so the scope is transparent before signature.

This is one of several legitimate paths — the right choice for any given team depends on whether they want an Odoo Gold Partner, the paid Odoo upgrade service, an iPaaS-first approach, an OCA-and-OpenUpgrade route, or a specialist migration vendor. Explore FlitStack →

Section 09

Frequently asked questions

The eight questions every Odoo ERP migration team works through before they sign the scope.

References

Sources

  1. 1 Odoo — Wikipedia
  2. 2 Is Odoo the right ERP for my company? — r/Odoo (50M USD / 400 user evaluation)
  3. 3 SAP wants a 20% price increase — r/NoStupidQuestions (vendor-cost exit)
  4. 5 Replacing our legacy ERP with Odoo Enterprise — r/Odoo (brick-and-mortar SME)
  5. 6 JTL-Wawi to Odoo migration war story — r/Odoo
  6. 7 Migrating from Odoo.sh (Enterprise) to Self hosted (Community) — r/Odoo
  7. 8 Export and import data — Odoo 19.0 documentation
  8. 11 Odoo's annual major releases — r/Odoo (yearly breaking version cadence)
  9. 12 ORM reference — Odoo 19.0 documentation
  10. 13 Get started — Odoo 19.0 Accounting documentation (Chart of Accounts setup)
  11. 14 Managing Parent-Child Relationships Between Partners in Odoo — Odoo Forum
  12. 17 Export and import data — Odoo 14.0 documentation (Country/Database ID/External ID resolution)
  13. 18 Definitive foolproof steps for a successful import — Odoo Forum (Many2many comma-separated)
  14. 19 Upload size limit in Odoo — Odoo Forum (64 MiB Odoo.sh, 128 MiB self-hosted)
  15. 20 Fields and widgets — Odoo 19.0 documentation (Studio)
  16. 21 what is ondelete='cascade' in OpenERP — Odoo Forum
  17. 22 Batch Import — Odoo Forum (Odoo Online throttling)
  18. 27 Chart of Accounts and localisation — Odoo 19.0 Accounting documentation
  19. 30 What Does Odoo Migration Really Cost? Transparent Pricing Breakdown — Aspire Softserv
  20. 33 How to bulk insert record in Odoo — Odoo Forum (Model.create with list)
  21. 35 Error when importing Journal Entry — r/Odoo (unbalanced debits/credits)
  22. 36 Units of measure — Odoo 19.0 Inventory documentation
  23. 37 Units of measure and BOM in Odoo version 18.2 — Odoo Forum (tracking change blocked)
  24. 39 Manufacturing > Bill of Materials in Odoo — Odoo Forum (BOM structure)
  25. 41 Record exchange rates at payments — Odoo Accounting multi-currency documentation
  26. 42 Image file types supported in Odoo — Odoo Forum
  27. 43 Base Attachment Object Store (fs_attachment) — Odoo Apps Store
  28. 44 Import file and retain file date — Odoo Forum (create_date is upload time)
  29. 45 OCA Audit Log — The Odoo Community Association
  30. 46 Cannot import date field — Odoo Forum
  31. 47 Error when importing sample data — Odoo Forum (ondelete restrict)
  32. 52 Resellers and Implementation Partners — Odoo

Need help running this migration?

FlitStack AI runs Odoo ERP migrations end-to-end.

Fixed-fee pricing, a hands-on migration engineer, full field mapping and validation. The work described in this guide — done for you.