Odoo Development
Odoo Module Upgrade Scripts and Data Migrations
When a module version changes the meaning of stored data rather than just its shape, existing rows need converting. Odoo's mechanism is a folder of Python scripts named by version and phase, and the most common mistake is not writing a bad one, it is writing none and never noticing.
Part of our guide to Odoo Development
When you need one
You need a migration script whenever the new version of your module changes what existing rows mean, rather than simply adding to them.
Adding a field needs nothing: Odoo adds the column and existing rows get the default. Changing a field's type, splitting a model in two, renaming a field whose values must be preserved, or altering the meaning of a selection value all need a script.
The failure mode when you skip it is the reason this is worth care. The module installs perfectly onto data that is now wrong. Nothing errors. It surfaces weeks later as a report that will not reconcile.
The layout
my_module/
migrations/
19.0.2.0.0/
pre-rename_columns.py
post-populate_new_field.py
end-cleanup.py
0.0.0/
end-invariants.pyThe folder name is a module version, and Odoo runs the scripts for every version between the installed one and the one in the manifest. The scripts only run when the version actually changes, so bumping the manifest version is part of shipping the migration, not an afterthought.
Two special cases are worth knowing. A folder named 0.0.0 runs on any version change: its scripts run first in the pre phase and last in post and end, which makes it the right place for invariants you want checked on every upgrade. And a folder can be named with a server version prefix, such as 19.0.1.1, to run only on that series.
The three phases
pre- runs before the module is initialised, so the new fields do not exist yet and the old ones still do. This is where you rescue data you are about to lose: stash a column's values, rename a column so the ORM adopts the data instead of creating an empty one, prepare a lookup table.
post- runs after initialisation. The new schema exists and can be populated, so this is where computed conversions belong.
end- runs after every module in the update has loaded. Use it when your work depends on another module having finished, which is the only thing it is genuinely for.
The pre and post split matters more than it first appears. Renaming a column in pre- preserves the data, because the ORM then finds a column already holding values. Doing it in post- is too late: the ORM has already created the new column empty, and the old data is either gone or orphaned.
The function
Every script must define one function:
def migrate(cr, version):
if not version:
return
cr.execute("""
UPDATE project_task
SET priority = '1'
WHERE priority_old = 'high'
""")Odoo raises an error naming the file if migrate is missing, so that mistake is loud.
Two things about the arguments. cr is a raw database cursor, not an environment, so this is SQL by default. And version is the installed version, which is falsy on a fresh install, which is why the guard above matters: a fresh install has no old data to convert and should skip the work entirely.
Using the ORM instead of SQL
Raw SQL is fast and bypasses everything: no computed fields recompute, no constraints check, no automation fires. That is often exactly what you want during a migration, and occasionally the opposite.
When you need real ORM behaviour, build an environment:
from odoo import api, SUPERUSER_ID
def migrate(cr, version):
if not version:
return
env = api.Environment(cr, SUPERUSER_ID, {})
tasks = env["project.task"].search([("stage_id", "=", False)])
tasks.write({"stage_id": env.ref("project.stage_new").id})Be deliberate about the choice. ORM writes on a large table during an upgrade can be extremely slow, because each one triggers the recomputation chain. SQL on the same data is near instant and skips exactly the work you may or may not have wanted.
Test that it did something
The specific failure worth guarding against is a script that runs, raises nothing, and touches zero rows. That is indistinguishable from a script that was never written.
So count. Before and after, in the script itself:
import logging
_logger = logging.getLogger(__name__)
def migrate(cr, version):
if not version:
return
cr.execute("SELECT COUNT(*) FROM project_task WHERE priority_old = 'high'")
before = cr.fetchone()[0]
cr.execute("UPDATE project_task SET priority = '1' WHERE priority_old = 'high'")
_logger.info("Migrated %s tasks (expected %s)", cr.rowcount, before)Then run it against a copy of production, not a demo database. The rows that break migrations are the historical ones with unusual values, and a seeded database contains none of them by definition.
Make it safe to run twice
Upgrades get retried. A script that assumes it has never run before will corrupt data on the second attempt.
Write conversions to be idempotent: filter on the state you are converting from, so a second run finds nothing left to do. UPDATE ... WHERE priority_old = 'high' is safe to repeat. A script that increments or appends is not.
Where this sits in a bigger upgrade
Module migration scripts handle your module's own data. They are one part of a version upgrade rather than the whole of it, and the surrounding project work, testing against production data, validating the result and deciding a rollback deadline, is covered in migrating custom modules.
Odoo 19 also ships odoo-bin upgrade_code, which rewrites known renames in your source automatically. It handles syntax. Everything on this page is data, and no rewrite script can convert data whose meaning changed.
FAQ
Questions, answered.
Where do Odoo migration scripts go?
In a migrations folder inside the module, with a subfolder per version, containing files whose names start with pre-, post- or end-. Odoo runs them when the installed version differs from the manifest version, so nothing happens unless you also bump the version.
What function does an Odoo migration script need?
A function called migrate that takes a database cursor and the installed version: migrate(cr, installed_version). Odoo raises an error if the file does not define it, naming the file, so this failure at least is loud.
What is the difference between pre, post and end scripts?
pre runs before the module is initialised, so it is where you rescue data you are about to lose. post runs after, when new fields exist and can be populated. end runs after every module has loaded, which is where cross-module consistency work belongs.
Keep reading
More on Odoo Development.
The full guide, plus the other articles in this cluster.
With CODEerts
Need something built properly?
We are certified Odoo partners. We build custom Odoo modules that extend the framework rather than fight it, and we publish our own apps on the Odoo Store.
See how we can helpBook a callReady to make Odoo work the way your business does?
Book a free callCODEerts is a team of certified Odoo partners and full-stack engineers. We implement, customise and support Odoo ERP, then build the software around it.