This article is currently available in English only. Translation coming soon.
By the end of this recipe, you will know how to import a CSV of any Odoo model — contacts, products, sales orders, journal entries — without breaking sequence numbers, exhausting database connections, or producing the dreaded "Cannot import field X" error wall. Skill required: Odoo administrator comfortable with spreadsheets and basic SQL. Time required: 1 hour for a clean import; 4 hours when the source data is dirty (which it always is). ECOSIRE has imported 150 million records across client onboardings and the recipe below is the deduplicated lessons.
The reason most CSV imports fail: Odoo's import wizard is forgiving for small datasets (<10,000 rows) and unforgiving for large ones. The wizard runs everything in a single PostgreSQL transaction, which means one bad row at row 50,000 rolls back the whole thing. Worse, Many2one references using the human-readable "name" field are O(n) lookups — a 100k-row import joins against a 10k-row partner table 100 thousand times. With external IDs the same import drops from 6 hours to 12 minutes.
What you will need
- Odoo version: 17, 18, or 19. The import wizard at
/odoo/importis identical across these versions. - CSV file: UTF-8 encoded, comma-separated, with a header row matching field labels OR field technical names.
- User access: a user with create + write permissions on the target model.
- Time: 1 hour clean, 4 hours dirty data. Allow 30 minutes for testing on a copy of production before importing live.
- Backup: always run the import on a fresh database copy first. The Odoo wizard supports rollback only within a single batch — once committed, you need a database-level restore to undo.
Step-by-step
1. Always use external IDs for references
The single biggest performance and reliability win is using external IDs instead of name-based references. Add an id column to every CSV with a unique string:
id,name,email,country_id/id
__import__.partner_001,Acme Corp,[email protected],base.us
__import__.partner_002,Beta Ltd,[email protected],base.uk
The country_id/id notation looks up res.country by external ID, not by name. base.us is the pre-shipped external ID for United States. Use __import__ as the prefix so re-imports update the same records instead of duplicating.
Verification: paste the CSV into the wizard at /odoo/import and click "Test". The dry-run should show "0 errors" and the country_id field should display "United States" in the preview.
2. Set the encoding correctly
Save the CSV as UTF-8. Excel on Windows defaults to cp1252 (Windows-1252), which corrupts em-dashes, accented characters, and currency symbols. In Excel: File > Save As > "CSV UTF-8 (Comma delimited)". In LibreOffice Calc: Save As > Edit Filter Settings > Character Set "UTF-8".
# Verify the encoding from terminal
file -i your-import.csv
# Expected: charset=utf-8
# If you see charset=iso-8859-1 or unknown, convert:
iconv -f cp1252 -t utf-8 your-import.csv > your-import-utf8.csv
Verification: file -i returns charset=utf-8. Open the file in any modern editor and confirm special characters render correctly.
3. Preserve sequences when importing transactions
If you're importing 5,000 historical sales orders and you want them to keep their original numbers (SO00001, SO00002, ...), do NOT let Odoo auto-generate names. Add a name column to the CSV and set the values to your historical references.
But there's a trap: after import, the next new SO from the user interface starts at SO00001 again because the ir.sequence table doesn't know your imported numbers exist. Fix this by updating the sequence:
-- After importing 5,000 sales orders with names like SO00001 to SO05000
SELECT setval(
pg_get_serial_sequence('sale_order', 'id'),
(SELECT MAX(id) FROM sale_order) + 1
);
-- Update the Odoo sequence too
UPDATE ir_sequence
SET number_next = 5001, number_next_actual = 5001
WHERE code = 'sale.order';
Verification: create a new SO from the UI. It should be numbered SO05001, not SO00001 (or whatever the gap was).
4. Import in dependency order
Odoo enforces foreign-key constraints. If you import sales orders before the partners they reference, the import fails. Order matters:
- Companies and currencies (usually pre-shipped, skip).
res.countryandres.country.state(already there).res.partner(companies first, then individuals withparent_id).product.templateandproduct.product.sale.orderheaders, thensale.order.lineitems.account.moveheaders, thenaccount.move.lineitems.
Verification: each step's import wizard test shows zero errors before you click "Import" on the next.
5. Use the count_max= query string for large imports
By default the wizard previews only the first 5 rows. For a 100k-row CSV that's not enough sanity check. Append ?count_max=200 to the import URL to preview 200 rows:
https://your-odoo.com/odoo/import?count_max=200
This catches encoding issues, malformed dates, and broken Many2one references before you commit to a full import.
6. Batch large imports
The web wizard times out around 5 to 10 minutes per batch depending on your Nginx config. For imports over 50,000 rows, split into chunks of 10,000. Or use the odoo-bin import CLI:
sudo -u odoo /opt/odoo/venv/bin/python /opt/odoo/odoo/odoo-bin shell -c /etc/odoo/odoo.conf -d production <<EOF
import csv
batch_size = 1000
with open('/tmp/partners.csv') as f:
reader = csv.DictReader(f)
rows = list(reader)
total = len(rows)
print(f'Importing {total} rows in batches of {batch_size}')
for i in range(0, total, batch_size):
chunk = rows[i:i+batch_size]
self.env['res.partner'].load(
list(chunk[0].keys()),
[list(row.values()) for row in chunk]
)
self.env.cr.commit()
print(f' Imported {i+len(chunk)}/{total}')
EOF
Verification: the script prints incremental progress every 1,000 rows and finishes without OperationalError.
7. Validate post-import
Always run sanity queries after a large import:
-- Total count matches CSV row count
SELECT COUNT(*) FROM res_partner WHERE create_date >= CURRENT_DATE;
-- Partners without country are usually broken Many2one references
SELECT COUNT(*) FROM res_partner WHERE country_id IS NULL AND is_company;
-- Email duplicates
SELECT email, COUNT(*) FROM res_partner WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*) > 1;
Verification: count matches expectation, broken-reference count is zero, no unexpected duplicates.
8. Document what you imported
Keep a row in the project log:
2026-05-04 14:32 UTC | Imported 4,832 res.partner records | source file: /imports/customers-2026-04.csv | external_id_prefix: __import__.partner_2026
This pays off six months later when someone asks "where did this customer come from".
Common mistakes
- Importing without external IDs and re-running the import. Each run creates duplicate records. Always use
idcolumn with__import__.somethingprefix. - Excel + cp1252 encoding silently corrupting accents. Always re-save as UTF-8 before uploading.
- Importing currency-amount fields with thousand separators (e.g., "1,234.56"). Odoo expects "1234.56" — strip commas first.
- Date format mismatch. Odoo expects ISO 8601 ("2026-05-04"). US-format "5/4/2026" is rejected on European-locale instances.
- Hitting the 5-minute web timeout. Switch to CLI or split into smaller batches.
- Forgetting to update
ir.sequenceafter importing historical numbered records. New records start back at 1.
Going further
Pre-validation script: write a Python script that loads the CSV with csv.DictReader, validates each row against a schema (using pydantic or simple type checks), and writes a errors.csv of bad rows. Saves hours on dirty datasets.
External ID strategy for ongoing imports: design a stable mapping like __import__.{source_system}_{primary_key}. A daily sync from Salesforce uses __import__.sf_001A0000003abcd — it's idempotent and traceable.
XLSX support: Odoo 17+ accepts .xlsx directly. Useful for clients who refuse to open CSVs in Excel.
Automated import flows: build a small NestJS endpoint or Python script that watches an SFTP folder, runs the import, and notifies Slack on completion or failure. We have built this for clients with daily ERP-to-Odoo sync.
For complex multi-system migrations (SAP > Odoo, NetSuite > Odoo, QuickBooks > Odoo with 1M+ records), our Odoo migration team does this end to end. Pair this with how to write an Odoo migration script for the version-upgrade case.
Frequently Asked Questions
Why does my import say "Multiple records found"?
Odoo's name-based Many2one resolution is exact-match but case-insensitive. If two partners share a name (e.g., "John Smith"), the import cannot disambiguate. Switch to external ID resolution.
Can I update existing records via CSV?
Yes — set the id column to the existing record's external ID (or its database id with a .id suffix). Re-run the import; Odoo updates the matching records and creates new ones for new IDs.
What is the row limit?
There is no hard row limit, but the web wizard times out around 5 minutes. For 50k+ rows, use the CLI/odoo-bin shell approach with batching.
Does the import fire compute fields?
Yes for stored compute fields (they recompute as part of create() and write()). No for non-stored compute fields (they compute on read). After bulk imports, you may want to trigger a manual recompute via self.recompute(['field_name']) for non-stored fields.
For migrations that involve transforming the source data (currency conversion, address normalization, deduplication), ECOSIRE migration services handle the full ETL pipeline. Or read how to back up and restore an Odoo database with zero downtime before running any large import.
تحریر
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Odoo ERP کے ساتھ اپنے کاروبار کو تبدیل کریں
آپ کے کاموں کو ہموار کرنے کے لیے ماہر Odoo کا نفاذ، حسب ضرورت، اور معاونت۔
متعلقہ مضامین
How to Add a Custom Button to an Odoo Form View (2026)
Add custom action buttons to Odoo 19 form views: Python action method, view inheritance, conditional visibility, confirmation dialogs. Production-tested.
How to Add a Custom Field in Odoo Without Studio (2026)
Add custom fields via custom module in Odoo 19: model inheritance, view extension, computed fields, store/non-store decisions. Code-first, version-controlled.
How to Add a Custom Report in Odoo Using External Layout
Build a branded PDF report in Odoo 19 using web.external_layout: QWeb template, paperformat, action binding. With print logo + footer overrides.