Import a spreadsheet of products once, and running the same import again a week later — after a handful of prices changed — creates a second copy of every row instead of updating the first. The fix isn’t a different import tool; it’s one column most CSV templates leave out entirely.

The short version: if an imported file includes an id (External ID) or Database ID column, Odoo checks whether a record with that identifier already exists — updating it if so, creating it if not — instead of always creating a new row. The same file becomes safe to re-import indefinitely, which is precisely what a plain import without that column can never be.

Why a plain re-import duplicates everything

Without an identifying column, Odoo’s importer has no way to recognize “this row represents the same real-world record as one I already imported” — every row in the file becomes a create() call, every single time, regardless of whether an equivalent record already exists. Two imports of the same file, a week apart, produce twice as many records, not an updated version of the first batch.

Adding the id column changes this entirely

Per Odoo’s own documentation on data import/export: if an imported file contains a column mapped to External ID (or Database ID), Odoo checks for an existing record under that identifier first. If one exists, the import updates it; if not, it creates a new one carrying that identifier for next time. Re-importing the exact same file after editing a handful of rows updates only what changed and leaves everything else untouched — the entire reason this is worth setting up before the first import, not after duplicates already exist.

id,name,list_price,default_code
my_module.product_widget_a,Widget A,49.99,WIDGET-A
my_module.product_widget_b,Widget B,79.99,WIDGET-B

The naming convention that matters

An external ID has to be unique across every record of every model in the database, not just within one import file — which is exactly why the documentation’s own recommendation is to prefix it with the name of the application, module, or source system it came from (my_module.product_widget_a, not just product_widget_a). Without a prefix, two unrelated imports (or an import and a hand-written module) can collide on the same plain identifier by coincidence, silently overwriting a record neither import intended to touch.

Watch out: once a re-importable file is in regular use, treat its id column as permanent — changing an identifier between two runs breaks the exact mechanism that made re-importing safe, since the importer now sees what looks like a brand-new record rather than an update to the one it already created under the old identifier.

A column named field/id (or field/External ID) lets one import reference a related record by its own external identifier, rather than an internal database id that could differ across environments:

id,name,category_id/id
my_module.product_widget_a,Widget A,my_module.category_hardware

This is the same mechanism, applied to relations instead of the record itself — and it’s what makes an import portable across a dev database, a staging copy, and production, none of which share the same internal numeric ids for category_id, but which can share the same external identifier if it was defined consistently (in a module’s own data files, or in a prior import using this same convention).

When a re-importable file and a module’s own data collide

A module’s XML/CSV data files loaded via data/demo and a CSV import through the UI both ultimately create records with external ids in the same ir.model.data table — which means an import file re-using an identifier a module’s own data file already defined doesn’t create a duplicate, it updates that same record, exactly as if it were re-importing its own earlier run. This is usually desirable (a spreadsheet meant to customize a module’s own default data on top of what it ships), but worth being deliberate about: an import accidentally re-using an id from someone else’s module silently overwrites that module’s own record rather than creating a new one, which is exactly the collision the naming-prefix convention above exists to prevent.

Quick reference

GoalWhat to add
Make an import file safe to re-run after editsAn id column with a unique, prefixed external identifier per row
Reference a related record portably across environmentsA field/id column instead of the related model’s raw database id
Avoid silently colliding with an unrelated recordPrefix the external id with the module/source name, always

Frequently asked questions

Does the id column need to match an existing XML data file's external ids?

Not necessarily — a CSV import can define brand-new external ids that have never existed anywhere else; what matters is that once assigned, an id stays associated with the same logical record across every future re-import of that file.

Can I use Database ID instead of External ID for the same effect?

Yes, functionally — but a Database ID is specific to one single database and won't match up correctly if the same file is later imported into a different environment (a staging copy, a fresh install), which is exactly the portability an External ID provides instead.

What happens if two different rows in the same file accidentally use the same id?

The second row updates the same record the first row just created or updated, rather than creating a distinct one — from the importer's perspective, they're the same logical record by definition, since that's exactly what a shared external id means.

Is this the same external ID mechanism used by ir.model.access.csv and ir.rule records?

Yes — it's the same underlying concept covered in the manifest.py article's discussion of data file loading order: a stable, human-assigned identifier for a specific record, usable consistently across XML data files, CSV imports, and code (ref()/env.ref()) alike.

Should every CSV import include an id column, even a one-time import that'll never be re-run?

Not strictly necessary for a genuine one-off, but it costs nothing to include and means a "one-off" import stays safe to re-run later if it turns out not to have been one-off after all — a common enough outcome that including it by default is the safer default habit.

Further reading

And on this site: The manifest.py File, Key by Key for how this same external-id convention shows up in a module’s own data files, and the External ID / XML ID Helper for suggesting a consistent, conventionally-prefixed identifier rather than inventing one per import.