Database Migration in Odoo
By Braincuber Team
Published on January 28, 2026
Upgrading from Odoo 16 to 17 or 17 to 18 isn't just clicking a button. Your database schema changes, fields get renamed or removed, data types transform, computed fields need recalculation, and custom modules require adaptation. Skip any step and you're looking at broken workflows, missing data, or a system that won't start. This guide covers the complete migration process—what breaks, why it breaks, and how to fix it.
Database migration involves three phases: pre-migration (analysis and preparation), migration (schema transformation), and post-migration (validation and cleanup). Each phase has specific technical requirements that determine whether your upgrade succeeds or fails.
Critical Warning: Never migrate production without testing on a copy first. Migrations are destructive—once data transforms, rolling back requires your backup. Test every workflow, report, and integration before go-live.
Migration Phases Overview
Pre-Migration Analysis
Audit models, fields, and data types. Identify removed/renamed elements. Create migration scripts for schema changes.
Schema Transformation
Execute SQL scripts for datatype changes, rename columns, update foreign keys, handle many2many pivot tables.
Post-Migration Validation
Recompute fields, validate views, test workflows, verify permissions, check scheduled actions and reports.
Common Breaking Changes Between Versions
| Change Type | Example | Impact |
|---|---|---|
| Model Renamed | mail.group → discuss.channel | SQL references, Python code, XML views break |
| Field Removed | sale.order: payment_term field | Custom reports/views using field fail |
| Datatype Changed | account.account.code: Char → JSONB | SQL queries return unexpected format |
| Computed Logic Changed | Different compute method for amount_total | Values differ from expectations |
| Module Merged | website_sale_wishlist merged into website_sale | Module dependencies fail to resolve |
| API Changes | @api.multi removed in v13+ | Custom modules crash on startup |
Pre-Migration: Analysis & Preparation
Backup Database & Filestore
Create a complete backup before any migration work. Test the backup on a separate server.
# Create database backup pg_dump -Fc -f /backups/odoo_v16_$(date +%Y%m%d).dump your_database # Backup filestore tar -czvf /backups/filestore_$(date +%Y%m%d).tar.gz /var/lib/odoo/filestore/your_database # Verify backup can be restored createdb test_restore pg_restore -d test_restore /backups/odoo_v16_$(date +%Y%m%d).dump # Test Odoo starts with restored database ./odoo-bin -d test_restore --stop-after-init
Audit Models & Fields
Compare your database schema against the target version to identify breaking changes.
import psycopg2
def audit_database(db_name):
"""Extract all models and fields from current database."""
conn = psycopg2.connect(dbname=db_name)
cur = conn.cursor()
# Get all tables (models)
cur.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
""")
tables = [row[0] for row in cur.fetchall()]
schema = {}
for table in tables:
cur.execute("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = %s
ORDER BY ordinal_position
""", (table,))
schema[table] = [
{'name': row[0], 'type': row[1], 'nullable': row[2]}
for row in cur.fetchall()
]
conn.close()
return schema
# Compare schemas
old_schema = audit_database('odoo_v16')
new_schema = audit_database('odoo_v18_empty')
# Find removed tables
removed = set(old_schema.keys()) - set(new_schema.keys())
print(f"Removed tables: {removed}")
# Find changed columns per table
for table in old_schema:
if table in new_schema:
old_cols = {c['name']: c['type'] for c in old_schema[table]}
new_cols = {c['name']: c['type'] for c in new_schema[table]}
removed_cols = set(old_cols.keys()) - set(new_cols.keys())
if removed_cols:
print(f"{table}: removed columns {removed_cols}")
Identify Datatype Changes
Some fields change from varchar to jsonb, integer to float, or text to html. These require SQL transformation.
-- Example: account.account.code changed from varchar to jsonb in Odoo 17+
-- Check current type
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'account_account' AND column_name = 'code';
-- If varchar, prepare transformation script
-- This runs during migration phase
ALTER TABLE account_account
ADD COLUMN code_new jsonb;
UPDATE account_account
SET code_new = jsonb_build_object('en_US', code);
ALTER TABLE account_account DROP COLUMN code;
ALTER TABLE account_account RENAME COLUMN code_new TO code;
Version-Specific Changes: Check Odoo's upgrade documentation for your specific version pair. Changes vary significantly between 14→15, 15→16, 16→17, and 17→18.
Migration: Schema Transformation
Create Pre-Migration Scripts
OpenUpgrade uses pre-migration scripts to prepare the database before Odoo's ORM runs.
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
"""Pre-migration script for custom module."""
cr = env.cr
# Rename deprecated field before ORM loads
if openupgrade.column_exists(cr, 'sale_order', 'old_field_name'):
openupgrade.rename_columns(cr, {
'sale_order': [('old_field_name', 'new_field_name')],
})
# Rename model (table) if it changed
if openupgrade.table_exists(cr, 'old_model_name'):
openupgrade.rename_tables(cr, [
('old_model_name', 'new_model_name'),
])
# Handle removed field - preserve data temporarily
if openupgrade.column_exists(cr, 'res_partner', 'deprecated_field'):
cr.execute("""
ALTER TABLE res_partner
RENAME COLUMN deprecated_field TO deprecated_field_backup
""")
Handle Many2many Pivot Tables
Many2many relationships use pivot tables that may need renaming when model names change.
-- List all pivot tables (usually named model1_model2_rel) SELECT table_name FROM information_schema.tables WHERE table_name LIKE '%_rel' AND table_schema = 'public'; -- Rename pivot table if model renamed -- Example: mail_group renamed to discuss_channel ALTER TABLE mail_group_res_users_rel RENAME TO discuss_channel_res_users_rel; -- Update foreign key column names ALTER TABLE discuss_channel_res_users_rel RENAME COLUMN mail_group_id TO discuss_channel_id; -- Recreate foreign key constraint ALTER TABLE discuss_channel_res_users_rel DROP CONSTRAINT mail_group_res_users_rel_mail_group_id_fkey; ALTER TABLE discuss_channel_res_users_rel ADD CONSTRAINT discuss_channel_res_users_rel_discuss_channel_id_fkey FOREIGN KEY (discuss_channel_id) REFERENCES discuss_channel(id);
Post-Migration: Validation & Cleanup
Recompute Stored Fields
Computed fields with store=True need recalculation after migration.
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
"""Post-migration: recompute stored fields."""
# Recompute sale order totals
orders = env['sale.order'].search([])
orders._compute_amounts()
# Recompute invoice status
orders._compute_invoice_status()
# Recompute partner display_name
partners = env['res.partner'].search([])
partners._compute_display_name()
# Recompute stock move quantities
moves = env['stock.move'].search([('state', '=', 'done')])
moves._compute_product_qty()
# Force recompute using SQL (faster for large datasets)
env.cr.execute("""
UPDATE sale_order_line
SET price_subtotal = product_uom_qty * price_unit * (1 - discount/100)
WHERE price_subtotal IS NULL OR price_subtotal = 0
""")
Validate Views & Fix Errors
Views referencing removed fields will crash. Identify and fix before users access them.
def validate_all_views(env):
"""Check all views render without errors."""
View = env['ir.ui.view']
errors = []
for view in View.search([('type', 'in', ['form', 'tree', 'kanban'])]):
try:
# Attempt to read arch with field validation
view._check_xml()
except Exception as e:
errors.append({
'view_id': view.id,
'name': view.name,
'model': view.model,
'error': str(e)
})
return errors
# Run validation
errors = validate_all_views(env)
for err in errors:
print(f"View {err['name']} ({err['model']}): {err['error']}")
# Fix by removing problematic view inheritance
# Or update the view to use correct field names
Test Critical Workflows
Validate business processes across all modules before production deployment.
Testing Checklist:
- Sales: Quote → Order → Invoice → Payment flow
- Purchase: RFQ → PO → Receipt → Bill flow
- Inventory: Receipts, deliveries, internal transfers
- Accounting: Journal entries, bank reconciliation, reports
- HR: Employee records, payslips, leaves
- Scheduled Actions: Verify all crons execute correctly
- Email Templates: Check rendering with new fields
- Reports: PDF generation for invoices, SO, PO
Migration Tools
OpenUpgrade
Open-source migration framework. Provides pre/post migration scripts for standard modules.
github.com/OCA/OpenUpgrade
Odoo.sh Upgrade Platform
Official Odoo upgrade service. Handles standard module migration automatically.
upgrade.odoo.com
openupgradelib
Python library with helper functions for writing migration scripts.
pip install openupgradelib
Conclusion
Database migration is surgical work. You're restructuring a live system while preserving years of business data. The process demands thorough schema analysis, careful SQL transformations, and exhaustive post-migration testing. Shortcuts lead to corrupted data, broken workflows, and frustrated users. Plan meticulously, test extensively, and keep your backup ready.
Migration Success Factors: Complete backup before starting. Audit all custom modules for compatibility. Run on test database first. Document every schema change. Recompute all stored fields. Validate every critical workflow. Have rollback plan ready.
