Ad – 728×90
⚡ Advanced Topics

Odoo Deployment and Upgrades – Production Best Practices

Deploying and upgrading Odoo modules in production requires care: the wrong approach can corrupt data or leave the system in an inconsistent state. The key tools are the version field in __manifest__.py, migration scripts, and install/upgrade hooks that run before and after module loading.

⏱️ 22 min 🎯 Advanced 📅 Updated 2026
What you'll learn:
  • Module versioning and what triggers an upgrade
  • The upgrade process: what odoo-bin -u does
  • Writing migration scripts for data changes
  • Using pre_init_hook and post_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:

Python
{
    '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:

  1. Load existing database state (old version)
  2. Run pre_migrate() scripts (if version changed)
  3. Apply new field definitions (ALTER TABLE for new columns)
  4. Load XML data files marked with noupdate="0"
  5. Run post_migrate() scripts
  6. 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):

Python
# 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.

Ad – 728×90

Install and Upgrade Hooks

Hooks in __manifest__.py run Python functions at specific points during install/upgrade:

Python
# __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

StepAction
1Backup production database before any upgrade
2Test upgrade on a copy of production data first
3Put Odoo in maintenance mode (stop web workers)
4Deploy new module code to the server
5Run odoo-bin -u my_module --stop-after-init
6Check logs for errors before restarting web workers
7Start web workers and verify functionality
8Clear browser cache / asset regeneration if needed
Key takeaways:
  • Bump the last two version numbers in __manifest__.py to trigger migration scripts
  • Put pre-migration data transforms in pre_migrate.py; post-migration backfills in post_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 ....