OpenUpgrade Migration Toolchain for Custom Odoo Modules#
What Is OpenUpgrade?#
OpenUpgrade is the open-source project that provides migration scripts for all standard Odoo modules when upgrading between major versions (e.g., 16 → 17). For custom modules, you write your own migrations/ scripts that follow the same conventions.
Migration Script Structure#
Place migration scripts inside your module:
my_module/
migrations/
17.0.1.0.0/
pre-migration.py ← runs BEFORE the module upgrade
post-migration.py ← runs AFTER the module upgrade
16.0.1.0.0/ ← version-specific folder (source version, not target)
...Odoo identifies which scripts to run by comparing the installed version stored in ir.module.module against the folder name.
Pre-Migration Script#
The pre-migration script runs before ORM is applied. Use raw SQL for safety:
# migrations/17.0.1.0.0/pre-migration.py
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
if not version:
return # fresh install, skip
# Rename a column that changed name in v17
openupgrade.rename_columns(env.cr, {
'my_model': [('old_field', 'new_field')],
})
# Convert a Selection field value that changed
env.cr.execute("""
UPDATE my_model
SET state = 'confirmed'
WHERE state = 'validate'
""")Post-Migration Script#
Post-migration runs after ORM is applied. Use the full Odoo ORM:
# migrations/17.0.1.0.0/post-migration.py
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
if not version:
return
# Recompute a stored field that changed formula
records = env['my.model'].search([])
records._compute_total_amount()
records.invalidate_recordset()
# Migrate data to a new Many2many replacing old Char
for rec in records:
if rec.legacy_tag_text:
tag = env['my.tag'].search([
('name', '=', rec.legacy_tag_text)
], limit=1)
if not tag:
tag = env['my.tag'].create({'name': rec.legacy_tag_text})
rec.tag_ids = [(4, tag.id)]openupgradelib Helpers#
The openupgradelib package provides utilities for common patterns:
from openupgradelib import openupgrade
# Rename model (updates ir.model, ir.model.fields, ir.rule, etc.)
openupgrade.rename_models(env.cr, [('old.model', 'new.model')])
# Rename XML ID
openupgrade.rename_xmlids(env.cr, [
('my_module.old_record_id', 'my_module.new_record_id'),
])
# Delete a model that no longer exists
openupgrade.delete_model_workflow(env.cr, 'my.obsolete.model')
# Check if a module is installed (use in conditionals)
if openupgrade.is_module_installed(env.cr, 'sale_management'):
# do something only when sale is present
pass
# Merge a field into another (copy data, then drop)
openupgrade.merge_columns(env.cr, 'sale_order', 'old_col', 'new_col')Column and Table Safety#
Never drop columns or tables in pre-migration. Mark them for deletion instead:
# pre-migration: rename to _migrated suffix so ORM won't touch it
env.cr.execute("""
ALTER TABLE my_model
RENAME COLUMN deprecated_field TO deprecated_field_migrated;
""")
# post-migration: drop after data is no longer needed
env.cr.execute("""
ALTER TABLE my_model DROP COLUMN IF EXISTS deprecated_field_migrated;
""")Handling Many2many Table Renames#
When a Many2many relation table changes name:
# pre-migration.py
openupgrade.rename_tables(env.cr, [
('old_m2m_rel_table', 'new_m2m_rel_table'),
])Version Guard Pattern#
Always guard your migration to skip fresh installs:
@openupgrade.migrate()
def migrate(env, version):
if not version:
# Module is being installed for the first time - no migration needed
return
# Migration logic hereTesting Your Migration#
- Clone the production database to a test instance.
- Run
odoo -d testdb -u my_module --stop-after-init. - Check logs for errors and warnings.
- Run your test suite against the migrated DB.
- Spot-check key records for data integrity.
Common Mistakes#
- Missing
@openupgrade.migrate()decorator - the script is silently skipped; always decorate themigrate()function - Using ORM in pre-migration - the ORM is not ready before the upgrade; stick to raw SQL in pre-migration
- Not guarding against fresh installs -
versionisNoneon first install; always checkif not version: return - Hardcoding database-specific IDs - never reference
idvalues directly; use XML IDs or domain searches - Forgetting to install openupgradelib - add it to the server requirements or run
pip install openupgradelibbefore the migration run

