CRM migration guide

The Definitive Guide to Migrating to Odoo CRM

Odoo CRM is an open-source, module-based pipeline tool whose import path rewards teams that pre-create models, respect platform throughput ceilings, and stage Community-vs-Enterprise feature gaps before the first CSV row is loaded.

22 min read 9 sections Updated May 27, 2026
Odoo CRM
Contacts
Companies
Deals
Leads
Activities
Notes

Inside this guide

What you'll learn, section by section

  1. 01

    Why teams migrate to Odoo CRM

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

  2. 02

    The Odoo CRM data model you need to map into

    Models, fields, 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 CRM

    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 programmatic loads

    Several 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 CRM

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

  6. 06

    The pitfalls that derail Odoo CRM migrations

    Nine 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 CRM migration team works through before they sign the scope.

Section 01

Why teams migrate to Odoo CRM

The four shapes an Odoo CRM 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 CRM is the sales-side application inside a suite of around 80 official modules — Sales, Inventory, Accounting, Manufacturing, HR, Project, Website, eCommerce — all sharing the same PostgreSQL database and ORM.

The typical Odoo CRM customer is a small-to-mid-market team — 10 to 300 users — that wants pipeline plus quoting plus invoicing plus inventory under one schema, and is willing to absorb the open-source learning curve in exchange for the integration depth. Compared with HubSpot or Pipedrive, Odoo positions itself on integrated business operations and Community-edition affordability; compared with Salesforce, it positions on a unified data model and a far smaller per-user cost on Enterprise tiers.

The shapes of migration that actually land on Odoo CRM tend to fall into four patterns. First, suite consolidation projects: a company replacing a stack of Pipeline CRM plus QuickBooks plus a separate inventory tool with the Odoo all-in-one suite. Second, vendor-cost exits from Salesforce or Zoho where total cost of ownership on smaller teams justified an open-source alternative 3.

Third, legacy replacements — older OpenERP/TinyERP installs, Sage, Microsoft Dynamics on-prem, or homegrown databases — where the source schema is loose and the project is really a re-architecture against crm.lead and res.partner. Fourth, Odoo-to-Odoo version migration, where a team running Community 15 jumps to 18 or 19, or moves from Odoo.sh Enterprise back to self-hosted Community 4.

Each shape has a different difficulty profile: a Pipeline CRM migration usually has clean object parity but messy custom-field labels, while a Salesforce migration carries rich automation that does not move across.

What makes migrating *to* Odoo CRM 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 56. Pre-built templates exist for Contacts, Leads, Companies and Products.

What makes it harder than the average is the Community-vs-Enterprise gap, where Studio, advanced reporting, the Marketing Automation app and several connector modules are paid-only and silently disappear if you restore an Enterprise database into a Community container 4. The second hard edge is the per-tenant throughput ceiling on Odoo Online of roughly one programmatic request per second — a constraint that catches teams scaling parallelism off database-server expectations 8.

Studio customisations, custom Python modules, automated actions and server actions 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, custom modules and server actions do not move with a CSV import — they are rebuilt in the destination.

Section 02

The Odoo CRM data model you need to map into

Models, fields, 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 CRM 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 9. Sales activity and pipeline behaviour sit on top of the same shared res.partner master record used by Accounting, Sales, and 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 CRM migration 10.

Object Stores Required on import Tier
res.partner Master record for individuals and companies (Contacts) name; is_company flag distinguishes person vs company All editions
crm.lead Leads and opportunities in the pipeline name (subject); type ('lead' or 'opportunity') All editions (CRM module)
crm.team Sales teams that own pipelines name; member_ids All editions
crm.stage Pipeline stages (Qualified, Proposition, Won) name; sequence; team_id (optional) All editions
crm.tag Free-form tags on leads/opportunities name All editions
mail.activity Scheduled to-dos: calls, meetings, emails, tasks activity_type_id; res_model; res_id; date_deadline All editions
mail.message Chatter messages, emails, internal notes per record model; res_id; body; message_type All editions
calendar.event Meetings synced with Outlook/Google name; start; stop; user_id All editions; sync setup is per-user
product.product / product.template Sellable items linked to quotes and orders name; type; list_price All editions
sale.order / sale.order.line Quotations and confirmed sales orders partner_id; order line products and quantities Requires Sales module

res.partner is the unifying record — a single row represents either a person (is_company=False) or a company (is_company=True), and the parent-child relationship between an employee and their employer is modelled by the parent_id Many2one field on res.partner itself 11.

Odoo does not have a single canonical natural-key field like HubSpot's email. Instead, the import tool uses an XML external ID — a string of the form __import__.123 or your_module.contact_acme — 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 5.

If you do not supply external IDs, Odoo creates 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 12. Custom field types determine validation and storage; the catalogue below covers what you can model and the limits you need to plan around 9.

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
Date / Datetime YYYY-MM-DD / YYYY-MM-DD HH:MM:SS in UTC Datetime stored UTC; ISO-8601 strongly recommended 13
Selection (picklist) Defined as Python tuple list Internal key vs label; import expects the key 9
Many2one (foreign key) Single related record Resolves by name OR external ID — pick one and be consistent
One2many / Many2many Inverse / junction relations Many2many uses comma-separated names on import
Boolean True/False/1/0 Empty cells default to False on import
Binary 128 MiB default attachment cap 14 Base64-encoded on programmatic load; large files belong in filestore
Custom fields via Studio Unlimited count, Enterprise-only UI Community: edit via Developer Mode → Settings → Technical → Fields

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 15. This is configured in fields.py per field, not at the database level you can flip from the UI.

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 16. Either way, the model must exist in the database before records of that type can be imported.

Section 03

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

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 CRM migration is how much work you do on the source side before the first import button is pressed. Odoo's own implementation guide warns that data preparation — not the upload itself — is where most projects lose days, 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, owner emails resolved to existing res.users records, and every row stamped with a stable id column.

Source-side prep

  • Audit and dedup the source database before export — Odoo's Data Cleaning app handles post-import dedup on res.partner, but it is much cheaper to dedup on the source than to merge after 12. Match on email, phone, and VAT number.
  • Normalise dates to YYYY-MM-DD or YYYY-MM-DD HH:MM:SS in UTC. Odoo's importer auto-detects most formats but warns that day/month inversions like 01-03-2016 are ambiguous; use ISO-8601 to remove the guesswork 13.
  • Convert selection field labels to internal keys — for stage names, lead status, and any custom Selection field, Odoo expects the Python key (e.g. done, cancel), not the display label. Mismatches fail on import with cryptic errors 9.
  • 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.contact_42. This is what makes re-runs and reconciliation deterministic 5.
  • Decide what is in scope for historical activities. mail.activity and mail.message rows can be imported, but the chatter history from a separate CRM rarely preserves cleanly without custom scripting against the ORM.

Destination-side prep

  • Create a staging database on Odoo.sh (staging branches are first-class) or a duplicate database on Odoo Online/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 the Sales/Salesperson groups and a crm.team. Owner assignment fails silently if the referenced user does not exist 20.
  • 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.
  • Build pipelines, stages and teams in CRM → Configuration → Stages and Sales Teams before importing leads — rows targeting a non-existent stage_id fall back to the default stage rather than failing loudly.
  • Install required modules first — Sales, CRM, Contacts, Discuss (for chatter), Calendar (for activities), and any community OCA modules 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 — and communicate it to every department that touches the CRM. Train sales reps on Odoo's kanban pipeline drag-and-drop, the chatter and activity widgets, and the Discuss inbox before go-live, not after.

A typical small-business migration runs one to two business days; a Salesforce-to-Odoo project with deal history, custom modules and Studio rebuilds runs two to four weeks of elapsed time even when the technical work is faster 21. Build the human runway accordingly.

Section 04

Import mechanisms: UI wizard, base_import, and programmatic loads

Several 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. Programmatic loads handle repeatable and very large volumes. 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, Sales → Leads, etc.), click the Actions icon (or the gear menu in older versions), then Import records → Upload File 6. 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 Odoo Online and Odoo.sh enforce per-instance memory and CPU limits that practically cap a single import at the low hundreds of thousands of rows before the worker times out 5. 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 5. Re-imports against the same external ID in the id column update existing rows; new external IDs create.

Programmatic loads — XML-RPC and JSON-RPC transports

Odoo's programmatic load surface has existed since version 6 and exposes the same ORM methods as the UI 22. Calls invoke execute_kw(db, uid, password, model, method, args) with methods like create, write, search_read, and the bulk-friendly load.

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 request timeouts on Odoo.sh.

Choose the programmatic 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 throttle 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 via XML-RPC; 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 23. For Odoo Enterprise upgrades, Odoo S.A.'s paid upgrade service handles the schema transformation but is not available to Community users.

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, or any re-runnable load → XML-RPC / JSON-RPC with chunked load() calls and a back-off loop.

Section 05

Mapping your data into Odoo CRM

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

SOURCE ODOO CRM 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 migration earns its scars. The schema decisions you make in your mapping spreadsheet determine whether reports work on day two, whether automated actions fire correctly on day five, and whether your sales team trusts the data on day thirty.

Work model by model, top to bottom of the dependency order: res.users first (so owners can be referenced), then companies on res.partner (so contacts and leads can associate to them), then individual contacts on res.partner, then crm.lead, then mail.activity and mail.message, then sale.order if you are bringing in quote history.

Contacts (res.partner)

Common source → res.partner mapping

Source Destination
  • external ID / source primary key
    id (XML external ID column)

    The upsert key — re-running with the same id updates 5

  • first_name + last_name (person) or company_name
    name

    Concatenate for is_company=False; use company name when is_company=True

  • is_organisation flag
    is_company

    Boolean — True for companies, False for individuals

  • email
    email

    Lowercased; Data Cleaning app can dedup post-import 12

  • phone / mobile
    phone / mobile

    Normalise to E.164; both fields exist separately

  • parent_account_id
    parent_id (Many2one to res.partner)

    Resolve via external ID; import companies first, then employees

  • vat_number / tax_id
    vat

    Format-validated against country rules; strip on import if dirty

  • country / state
    country_id / state_id

    Many2one — must match an existing res.country / res.country.state record

  • language preference
    lang

    Use locale code (en_US, fr_FR); language pack must be installed

Companies and parent-child relations

Odoo collapses companies and contacts into the same res.partner table, distinguished by the is_company boolean and linked through parent_id. The clean import order is: companies first with is_company=True and stable external IDs, then individual contacts with is_company=False and parent_id/id pointing to the company's external ID 11.

If you are consolidating multiple source systems, run each through a transform step that adds a system prefix to the external ID (hubspot.contact_123, pipedrive.contact_456) so you can re-import deltas from either system without collision.

Leads, opportunities and pipelines (crm.lead, crm.stage)

crm.lead is the single table for both leads and opportunities — the type field stores 'lead' or 'opportunity', and conversion is a state transition rather than a record move. Recreate every pipeline and every stage in CRM → Configuration → Stages before importing — rows referencing a non-existent stage_id will be ignored or fall to the default stage.

If you are consolidating multiple source pipelines into Odoo, the cleanest approach is to keep them as separate sales teams (crm.team) with their own stages for at least 90 days post-migration, then merge once reporting has stabilised.

Common source → crm.lead mapping

Source Destination
  • opportunity_name / lead_subject
    name

    Required field — never null

  • stage / pipeline_stage
    stage_id

    Resolve to existing crm.stage via name or external ID

  • lead vs opportunity flag
    type

    Selection: 'lead' or 'opportunity' — use the internal key

  • amount / expected_revenue
    expected_revenue

    Float; default precision (16,2); set company currency_id

  • probability
    probability

    Float 0–100; default driven by stage

  • expected_close_date
    date_deadline

    YYYY-MM-DD; ISO-8601 strongly recommended 13

  • owner / salesperson
    user_id

    Many2one to res.users — pre-provision users first 20

  • sales team
    team_id

    Many2one to crm.team — defaults from user if omitted

  • primary contact
    partner_id

    Many2one to res.partner; resolves via external ID

  • tags
    tag_ids (Many2many)

    Comma-separated names; missing tags are created on import

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 weaken governance, 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 23.

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 (Odoo decorator-based or compute=) 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.

Historical activities, notes and chatter

Odoo splits historical engagement across three tables: mail.activity (scheduled to-dos with a date_deadline), mail.message (chatter messages and emails attached to a record), and calendar.event (meetings synced from Outlook/Google) 25. mail.message is what shows in the chatter widget on every record's right side, and it is the destination for migrated email and call history 26.

Bulk-importing historical chatter is non-trivial because mail.message enforces strict relations on model, res_id, author_id and threading via parent_id. The Odoo forum's general guidance is that note, message and activity history *can* be imported via the export/import feature, but typically requires a Python script against the ORM to handle threading and author resolution correctly 27.

For one-off imports under a few thousand messages, CSV via the wizard works; above that, script against mail.message.create with explicit model='crm.lead' and res_id set to the lead's database ID.

Activities (calls, meetings, to-dos) can be created with a backdated date_deadline and summary, but create_date on the activity itself cannot be overridden — it stamps to import day 28. If you need a historical timeline view, store the original timestamp in a custom legacy_date Datetime field on the activity and filter reports off that.

Files and attachments

Attachments in Odoo live on the ir.attachment model and link to any record via res_model and res_id. Default per-attachment size cap is 128 MiB and cannot be increased in Odoo Online; self-hosted instances can raise it in server configuration 14. Supported image formats include JPEG, PNG, GIF, BMP, TIFF, SVG, ICO, WEBP, PSD and EPS 29.

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 30, or (c) scripted upload via the ORM where you base64-encode the binary and create an ir.attachment record with datas, name, res_model and res_id set.

For large attachment estates, most teams adopt: keep originals in S3, install fs_attachment, and only inline-upload the most recent or most-referenced subset.

Audit trail, ownership and original timestamps

Odoo records create_date, create_uid, write_date and write_uid automatically on every model and does not preserve the original file's creation or modification date during importcreate_date stamps to upload time 28. If you need to preserve the original audit trail, create two custom fields per model — legacy_create_date (Datetime) and legacy_create_uid (Many2one to res.users) — and populate them from the source export.

Owner assignment during import works only if the res.users record exists at the moment of import; rows whose user_id cannot be resolved import with no owner set, which silently breaks team-based record rules and pipeline routing 20.

Account-level audit logging is not on by default. The OCA Audit Log module lets you define per-model rules in Settings → Technical → Audit → Rules and writes old/new values for every change 31. Without it (or the Enterprise Audit Trail in the Accounting app), there is no out-of-the-box per-field change history.

CRM-specific: lead scoring, email/calendar sync, automated actions

Lead-scoring rules do not import — they are rebuilt in CRM → Configuration → Lead Scoring Rules (Enterprise) or via custom server actions on Community. Plan to recreate the rule logic from your source platform documentation rather than trying to migrate accumulated score values; if you must preserve historical scores, store them in a legacy_score Float field and let the new rules accrue forward.

Email and calendar sync is configured per user under Settings → General Settings → Discuss / Calendar for Outlook (requires an Azure AD App Registration with Application ID and Client Secret) and Google Workspace 32. Set this up *after* user provisioning is complete but *before* the activity import runs, or you risk duplicate calendar entries when the connector backfills against the same events you are loading.

Automated Actions (the Odoo equivalent of HubSpot workflows) and Server Actions are stored in base.automation and ir.actions.server and do not move with a CSV import — they are rebuilt in the destination by an admin in Settings → Technical → Automation Rules.

Section 06

The pitfalls that derail Odoo CRM migrations

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

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 — CRM Enterprise, Documents, Social, WhatsApp, Studio, Marketing Automation — as 'installed' but 'not installable, skipped'. The UI then crashes with errors like View types not defined map found in act_window and Studio customisations vanish from the form views 4. Before you switch hosting tiers, list every installed module via Settings → Apps → Installed and confirm each has a Community equivalent or a replacement plan. 4

High impact

Selection field label vs internal key mismatch

Odoo Selection fields — crm.lead.type, crm.lead.priority, every custom dropdown — store an internal Python key distinct from the UI label. The label is Opportunity; the key is opportunity. 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 9. 9

High impact

Programmatic throughput ceiling at ~1 request per second on Odoo Online

Practitioners testing importers on Odoo 17 report the Odoo Online terms of use enforce a throughput ceiling of roughly one programmatic request per second 8. A migration loop tuned to dozens of parallel workers will throttle, retry, throttle and run for 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, not the public ceiling. 8

High 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 13. 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 33. Always set the Date Format explicitly in the wizard's Formatting Options to ISO-8601 (YYYY-MM-DD), or convert to ISO before export. 13

High impact

create_date cannot be overridden on import

Odoo does not preserve the original 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 28. 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 all historical-period reports to use the legacy fields. 28

Medium impact

Many2one resolution by name fails on duplicates

Many2one fields like user_id, partner_id, country_id can be imported by referencing the target record's name or its external ID. Name resolution is fragile: two users called *Sales Manager*, two countries called *Korea*, or a partner whose name has trailing whitespace will silently match the wrong record or none. Pick external ID resolution consistently — populate an id column on the lookup tables first, then use field_name/id in subsequent imports to resolve by the stable key 5. 5

Medium impact

Unicode / non-ASCII characters break older importers

Accented characters in usernames, French/German company names, or smart quotes pasted from Word can throw UnicodeEncodeError on Odoo 13 and earlier and Unsupported format character '{' (0x7b) in some templated import flows 3435. Save CSVs as UTF-8 with BOM, prefer XLSX where possible, and run a one-pass unicodedata.normalize('NFC', value) over text columns before export. Newer Odoo versions are far more tolerant but older Community installs still bite. 34

Medium impact

ondelete=restrict blocks model deletion in dependency chains

Relational fields in Odoo specify ondelete='cascade', 'restrict' or 'set null' per field 15. If you import test data into a sandbox, then try to delete a parent record (a res.partner referenced by a sale.order), Odoo blocks the delete with The operation cannot be completed: Another model is using the record you are trying to delete 36. Plan archival, not deletion, for parent records — use the active=False boolean to soft-delete and let related records keep their references. 15

Low impact

GDPR data residency on Odoo Online is configured at signup

Odoo S.A. operates EU and US data centres and is GDPR-compliant by default, but Odoo Online accounts pick a region at signup and changing it later requires a support-managed migration of the database to a different region 37. If your project requires EU residency and the account was created in the US, plan the regional move *before* the data migration, or you will move records twice. Self-hosted and Odoo.sh customers control region directly via the cloud provider they select. 37

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 guides 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 record counts and relation integrity, and a 30-day post-migration data-quality audit. The most reliable signal is having department reps verify their own records — they know what right looks like better than any reconciliation script.

Build a reconciliation queries spreadsheet that compares source and destination on each of these counts. Anything outside a 0.5 percent variance gets investigated before users get login access.

  • Total partners imported vs source — split by is_company True/False — minus deliberately excluded rows (role-based emails, bounced lists, opt-outs).
  • Total leads and opportunities per stage per team vs source, plus sum of expected_revenue per pipeline — a non-trivial revenue variance usually signals a stage-mapping error or a currency-precision drop.
  • Activities and messages per record — count mail.activity and mail.message rows against source-derived expectations, and confirm date_deadline and legacy_create_date round-tripped.
  • Parent-child integrity on res.partner — every contact with is_company=False should have a parent_id pointing to a valid is_company=True partner; orphans indicate a broken company-first import.
  • Owner distributionGROUP BY user_id on crm.lead and confirm no lead landed unowned that should not have.
  • External-ID uniqueness — query ir.model.data for duplicate (module, name) combinations on your namespace; any duplicates indicate the dedup transform missed cases.
  • Attachment counts per record — for migrated ir.attachment rows, count by res_model and compare against the source file inventory.

On top of reconciliation, run a manual spot-check protocol: pick 30 random records across models and verify each field against the source UI. Pick five high-value opportunities and trace the full relation graph — partner, sales team, owner, activities, chatter, attachments, related quotes. 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. The closest thing is the OCA Audit Log module (when enabled with the right rules) which captures old/new values per change 31, plus the Odoo.sh staging branches feature which lets you fork production and roll back at the database level. Neither is a one-click reversal — both 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 team is notified; (2) final delta export captures everything that changed during the test-import window; (3) delta is imported with the same external-ID namespace; (4) reconciliation runs; (5) users get login access and a 48-hour hyper-care window with the migration lead on call; (6) source decommission is scheduled for 30 to 90 days out, never the same day.

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. For CRM migrations specifically, partners with explicit Salesforce-to-Odoo, Zoho-to-Odoo or Pipeline-CRM-to-Odoo practices tend to ship cleaner than generalist implementation shops 21. The Odoo Community Association (OCA) maintains open-source modules — Audit Log, OpenUpgrade, fs_attachment, Data Cleaning OCA forks — that fill gaps left by the Enterprise feature set 2331.

Synavos, Aspire Softserv, Silent Infotech, Iqra Technology, Softeko, Braincuber 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 services 39. 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 23.

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 a migration is rarely the migration itself — it is the staging layer that lands source data into a warehouse, the transformation layer that converts Selection values and resolves Many2one relations, and the ongoing-sync layer that takes over once the one-time migration is complete.

Managed-migration cost ranges vary widely. Hourly rates for Odoo professional services run $10–$150+ per hour depending on region and partner expertise. A clean Pipeline-CRM-to-Odoo move of under 25,000 contacts with no historical chatter and standard models only often lands in the $3,000–$10,000 range.

A Salesforce-to-Odoo project with opportunity history, Studio customisations, historical activities and Marketing-Automation decoupling typically runs $20,000–$150,000+, with the upper end driven by record count, custom-model complexity, historical-data depth, and the number of integrations that need rebuilding rather than re-pointed 39.

Migration-platform tools that scope by record-count tier (free under 100 records, $149 under 5,000, $599 under 25,000, $2,499 under 500,000) sit at the lower end of the curve when the migration is simple and standard-object-only. The price climbs sharply once custom models, historical activities or chatter migration enter scope.

For teams that want to outsource the migration end-to-end, FlitStack specialises in Odoo CRM migrations and handles the external-ID design, Selection-key conversion, res.partner parent-child resolution, historical-data preservation and validation work described in Sections 5 and 7 of this guide. Pricing is fixed-fee, based on record count and source platform, with separate line items for custom models and historical chatter depth 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 CRM migration team works through before they sign the scope.

References

Sources

  1. 1 Odoo — Wikipedia
  2. 3 Best Zoho CRM Alternatives for Custom Development and Flexibility — r/CRMSoftware
  3. 4 Migrating from Odoo.sh (Enterprise) to Self hosted (Community) — r/Odoo
  4. 5 Export and import data — Odoo 19.0 documentation
  5. 6 Import products — Odoo 19.0 documentation
  6. 8 Odoo Online programmatic throughput limits — Odoo Forum
  7. 9 ORM reference — Odoo 19.0 documentation
  8. 10 Odoo CRM application reference — Odoo 19.0 documentation
  9. 11 Managing Parent-Child Relationships Between Partners in Odoo — Odoo Forum
  10. 12 Data Cleaning — Odoo 19.0 documentation
  11. 13 Datetime field reference — Odoo 19.0 documentation
  12. 14 Upload size limit in Odoo — Odoo Forum (128 MiB cap)
  13. 15 what is ondelete='cascade' in OpenERP — Odoo Forum
  14. 16 Fields and widgets — Odoo 19.0 documentation (Studio)
  15. 20 Configure Salesperson groups for own-documents access in Odoo 18 — Odoo Forum
  16. 21 Manufacturing Company Migrating from Salesforce to Odoo — Odoo Experience 2025
  17. 22 External transports for programmatic loads — Odoo documentation
  18. 23 Crowdfunding OpenUpgrade for Odoo Community 19.0 — r/Odoo (OCA)
  19. 25 Activities — Odoo 19.0 documentation
  20. 26 Communication in Odoo by email — Odoo 19.0 documentation
  21. 27 Migrate history CRM — Odoo Forum
  22. 28 Import file and retain file date — Odoo Forum (create_date is upload time)
  23. 29 Image file types supported in Odoo — Odoo Forum
  24. 30 Base Attachment Object Store (fs_attachment) — Odoo Apps Store
  25. 31 OCA Audit Log — The Odoo Community Association
  26. 32 Outlook Calendar synchronization — Odoo 19.0 documentation
  27. 33 Cannot import date field — Odoo Forum
  28. 34 User and unicode character error — Odoo Forum
  29. 35 unsupported format character on import — Odoo Forum
  30. 36 Error when importing sample data — Odoo Forum (ondelete restrict)
  31. 37 General Data Protection Regulation (GDPR) — Odoo
  32. 39 What Does Odoo Migration Really Cost? Pricing Breakdown — Aspire Softserv

Need help running this migration?

FlitStack AI runs Odoo CRM 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.