- Module versioning and what triggers an upgrade
- The upgrade process: what
odoo-bin -udoes - Writing migration scripts for data changes
- Using
pre_init_hookandpost_init_hook - Production deployment checklist
Module Versioning
The version field in __manifest__.py controls when migration scripts run. Use the format major.minor.patch.module_version where the first 3 numbers match the Odoo version:
{
'name': 'Library Management',
'version': '19.0.1.0.1', # Odoo 19, module version 1.0.1
# When you change module_version (last 2 numbers),
# Odoo will run migration scripts on next -u
}
When you run odoo-bin -u my_module, Odoo compares the installed version with the manifest version. If they differ, it runs migration scripts for all intermediate versions before applying the new code.
The Upgrade Process
Running odoo-bin -u my_module does the following in order:
- Load existing database state (old version)
- Run
pre_migrate()scripts (if version changed) - Apply new field definitions (ALTER TABLE for new columns)
- Load XML data files marked with
noupdate="0" - Run
post_migrate()scripts - Run
post_init_hook(if first install) or upgrade callbacks
XML data files with noupdate="1" are loaded only on first install and never again on upgrade — use this for default data that users may have modified.
Migration Scripts
Migration scripts handle data transformations when your module's data model changes. Place them in migrations/19.0.1.0.1/ (version-specific folder):
# migrations/19.0.1.0.1/pre_migrate.py
# Runs BEFORE new field definitions are applied
def migrate(cr, version):
"""Rename column before ORM recreates it."""
if not version:
return # first install, skip
cr.execute("""
ALTER TABLE library_book
RENAME COLUMN old_field_name TO new_field_name
""")
# migrations/19.0.1.0.1/post_migrate.py
# Runs AFTER new field definitions and XML data loading
def migrate(cr, version):
"""Backfill new column from existing data."""
if not version:
return
cr.execute("""
UPDATE library_book
SET full_title = title || ' by ' || author
WHERE full_title IS NULL
""")
Migration scripts receive the database cursor (cr), not the ORM environment — use raw SQL. The version argument is the previously installed version. If version is None, this is a fresh install.
Install and Upgrade Hooks
Hooks in __manifest__.py run Python functions at specific points during install/upgrade:
# __manifest__.py
{
'name': 'Library Management',
'version': '19.0.1.0.0',
'pre_init_hook': 'pre_init_hook',
'post_init_hook': 'post_init_hook',
'post_load': 'post_load',
}
# __init__.py
def pre_init_hook(env):
"""Runs before module tables are created.
Use for checks or pre-setup only — tables don't exist yet.
"""
pass
def post_init_hook(env):
"""Runs after first install only.
Tables exist; create default records here.
"""
env['library.category'].create({'name': 'General'})
def post_load():
"""Runs after module code is loaded but before install.
Use for monkey-patching third-party libraries.
"""
pass
Production Deployment Checklist
| Step | Action |
|---|---|
| 1 | Backup production database before any upgrade |
| 2 | Test upgrade on a copy of production data first |
| 3 | Put Odoo in maintenance mode (stop web workers) |
| 4 | Deploy new module code to the server |
| 5 | Run odoo-bin -u my_module --stop-after-init |
| 6 | Check logs for errors before restarting web workers |
| 7 | Start web workers and verify functionality |
| 8 | Clear browser cache / asset regeneration if needed |
- Bump the last two version numbers in
__manifest__.pyto trigger migration scripts - Put pre-migration data transforms in
pre_migrate.py; post-migration backfills inpost_migrate.py - Always backup and test on a production data copy before upgrading live
- Use
noupdate="1"on XML records that users may have modified
Frequently Asked Questions
What happens if a migration script fails?
Odoo wraps the upgrade in a database transaction. If a migration script raises an exception, the transaction is rolled back and the upgrade fails — your database remains in the pre-upgrade state. Fix the script and re-run. This is why testing on a copy first is critical: a failing migration on production could require a database restore.
When should I use a migration script vs a post_init_hook?
post_init_hook runs only on the first install and uses the ORM environment. Migration scripts run on every version bump and use raw SQL (no ORM). Use hooks for initial data seeding after install. Use migration scripts for structural changes (column renames, data transformations) when upgrading from a previous version.
How do I handle removing a field safely?
To remove a field: (1) in the new version, remove the field from your model Python class; (2) in the pre_migrate script, optionally save any data you need before the column is dropped; (3) Odoo does NOT automatically drop old columns — the column stays in PostgreSQL but becomes invisible to the ORM. If you need to clean up, add a post_migrate script that runs ALTER TABLE ... DROP COLUMN ....