Database Migration in Odoo 18: Complete Pre & Post Migration Guide
By Braincuber Team
Published on January 29, 2026
Upgrading your Odoo database from one version to another is more than a routine maintenance task—it's a strategic investment in your business's future. A well-executed migration ensures your ERP system stays secure, performs optimally, and gives you access to the latest features. But here's the reality: migrations can go wrong if you don't plan carefully.
I've seen companies lose weeks of productivity because they rushed into a migration without proper preparation. On the flip side, I've helped businesses complete seamless upgrades that took just a weekend. The difference? A methodical approach that covers every angle before, during, and after the migration.
In this guide, I'll walk you through the complete database migration process for Odoo 18, sharing practical insights from real-world projects. Whether you're upgrading from Odoo 16, 17, or even older versions, these principles will help you avoid common pitfalls and ensure a smooth transition.
- Pre-migration analysis: Models, fields, and schema validation
- Handling renamed, removed, and modified database structures
- Data type conversion strategies (especially for jsonb fields)
- Computed field population after migration
- View compatibility and UI validation
- Post-migration testing workflows
Why Database Migration Matters
Every Odoo release brings significant improvements—new features, security patches, performance optimizations, and framework updates. However, these changes often mean backend modifications that affect your existing data structure. Ignoring migrations means:
Security Vulnerabilities
Outdated versions lack critical security patches, exposing your business data to potential threats
Performance Degradation
Newer versions include query optimizations and caching improvements that boost system speed
Missing Features
New UI components, reporting tools, and workflow automations are only available in current releases
Community Support
Older versions eventually lose community and official support, making troubleshooting difficult
Phase 1: Pre-Migration Analysis
Before touching any data, your first job is to understand exactly what you're working with. This analysis phase is where most migration problems are prevented—not fixed after they occur.
Model Existence Verification
Start by comparing the models in your current database against the target Odoo version. Odoo occasionally renames, merges, or removes models entirely. For example, some specialized models from earlier versions might be consolidated into a single, more comprehensive model.
Use this SQL query to list all models in your current database:
-- List all registered models in the database
SELECT DISTINCT model
FROM ir_model
ORDER BY model;
-- Compare with installed modules
SELECT m.model, m.name, md.name as module
FROM ir_model m
JOIN ir_model_data md ON md.res_id = m.id
AND md.model = 'ir.model'
ORDER BY md.name, m.model;
Cross-reference this list against the official Odoo 18 model registry. Document any models that have been renamed or removed.
Field Structure Analysis
Field-level changes are more common than model changes. A field might be removed, renamed, or have its behavior completely altered. Some fields that were stored in the database might become computed fields (not stored), and vice versa.
-- List all fields for a specific model
SELECT f.name as field_name,
f.ttype as field_type,
f.store as is_stored,
f.compute as compute_method,
f.related as related_field
FROM ir_model_fields f
JOIN ir_model m ON f.model_id = m.id
WHERE m.model = 'sale.order'
ORDER BY f.name;
-- Find fields that might need migration attention
SELECT f.name, f.ttype, m.model
FROM ir_model_fields f
JOIN ir_model m ON f.model_id = m.id
WHERE f.ttype IN ('char', 'text', 'selection')
ORDER BY m.model, f.name;
Pay special attention to:
- Fields that were
store=Truebut are now computed - Fields that changed from
ChartoSelectionor vice versa - Related fields that might have different paths in the new version
Data Type Conversions
One of the trickiest migration challenges involves fields that change their underlying PostgreSQL data type. A notable example in recent Odoo versions is the shift from traditional varchar fields to jsonb for structured data storage.
The account.account.code field structure changed significantly. What was previously a simple Char field became code_store with jsonb type to support multi-company scenarios. Your migration script must handle this conversion explicitly.
-- Example: Converting varchar to jsonb for account codes
-- Step 1: Add the new column
ALTER TABLE account_account
ADD COLUMN IF NOT EXISTS code_store jsonb;
-- Step 2: Migrate data with proper JSON structure
UPDATE account_account
SET code_store = jsonb_build_object(
COALESCE(company_id::text, '1'),
code
)
WHERE code IS NOT NULL AND code_store IS NULL;
-- Step 3: Verify the conversion
SELECT id, code, code_store, company_id
FROM account_account
LIMIT 10;
Phase 2: Pre-Migration Script Development
Based on your analysis, you'll need to create migration scripts that handle all the structural changes. A well-organized pre-migration script should follow this pattern:
# migrations/18.0.1.0/pre-migrate.py
from odoo import api, SUPERUSER_ID
import logging
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""Pre-migration script for Odoo 18 upgrade."""
if not version:
return
_logger.info("Starting pre-migration from version %s", version)
# Step 1: Handle renamed models
handle_model_renames(cr)
# Step 2: Handle renamed fields
handle_field_renames(cr)
# Step 3: Handle data type conversions
handle_type_conversions(cr)
# Step 4: Clean up deprecated data
cleanup_deprecated_records(cr)
_logger.info("Pre-migration completed successfully")
def handle_model_renames(cr):
"""Rename tables for models that changed names."""
renames = [
# ('old_table_name', 'new_table_name'),
]
for old_name, new_name in renames:
cr.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = %s
)
""", (old_name,))
if cr.fetchone()[0]:
cr.execute(f'ALTER TABLE "{old_name}" RENAME TO "{new_name}"')
_logger.info("Renamed table %s to %s", old_name, new_name)
def handle_field_renames(cr):
"""Rename columns for fields that changed names."""
field_renames = [
# ('table_name', 'old_column', 'new_column'),
]
for table, old_col, new_col in field_renames:
cr.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = %s AND column_name = %s
""", (table, old_col))
if cr.fetchone():
cr.execute(f'''
ALTER TABLE "{table}"
RENAME COLUMN "{old_col}" TO "{new_col}"
''')
_logger.info("Renamed column %s.%s to %s", table, old_col, new_col)
def handle_type_conversions(cr):
"""Convert columns that changed data types."""
# Example: varchar to jsonb conversion
cr.execute("""
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'account_account'
AND column_name = 'code'
""")
result = cr.fetchone()
if result and result[1] != 'jsonb':
# Add new jsonb column if it doesn't exist
cr.execute("""
ALTER TABLE account_account
ADD COLUMN IF NOT EXISTS code_store jsonb
""")
# Migrate data
cr.execute("""
UPDATE account_account
SET code_store = jsonb_build_object(
COALESCE(company_id::text, '1'),
code
)
WHERE code IS NOT NULL
""")
_logger.info("Converted account_account.code to jsonb format")
def cleanup_deprecated_records(cr):
"""Remove records that are no longer valid in the new version."""
# Example: Remove deprecated ir.model.data references
cr.execute("""
DELETE FROM ir_model_data
WHERE model = 'ir.ui.view'
AND res_id NOT IN (SELECT id FROM ir_ui_view)
""")
deleted = cr.rowcount
if deleted:
_logger.info("Cleaned up %d orphaned ir.model.data records", deleted)
Phase 3: Computed Field Population
Here's something that catches many developers off guard: Odoo doesn't automatically populate computed fields during a raw database migration. After the schema migration, all those @api.depends decorated fields will show empty values until you explicitly trigger their computation.
Post-Migration Compute Script
Create a post-migration script that triggers the compute methods for all critical fields:
# migrations/18.0.1.0/post-migrate.py
from odoo import api, SUPERUSER_ID
import logging
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""Post-migration script for Odoo 18 upgrade."""
env = api.Environment(cr, SUPERUSER_ID, {})
_logger.info("Starting post-migration computed field population")
# Recompute sale order totals
recompute_sale_order_fields(env)
# Recompute invoice amounts
recompute_invoice_fields(env)
# Recompute stock quantities
recompute_stock_fields(env)
# Validate and fix user access rights
validate_user_access(env)
_logger.info("Post-migration completed successfully")
def recompute_sale_order_fields(env):
"""Recompute all computed fields on sale orders."""
SaleOrder = env['sale.order']
# Find orders that need recomputation
orders = SaleOrder.search([('state', '!=', 'cancel')])
_logger.info("Recomputing %d sale orders", len(orders))
# Trigger recomputation in batches
batch_size = 100
for i in range(0, len(orders), batch_size):
batch = orders[i:i + batch_size]
# Force recomputation of amount fields
for order in batch:
order._compute_amounts()
# Commit each batch
env.cr.commit()
_logger.info("Processed sale orders %d-%d", i, min(i + batch_size, len(orders)))
def recompute_invoice_fields(env):
"""Recompute invoice amounts and status fields."""
Invoice = env['account.move']
invoices = Invoice.search([
('move_type', 'in', ['out_invoice', 'out_refund', 'in_invoice', 'in_refund']),
('state', '!=', 'cancel')
])
_logger.info("Recomputing %d invoices", len(invoices))
batch_size = 50
for i in range(0, len(invoices), batch_size):
batch = invoices[i:i + batch_size]
for invoice in batch:
invoice._compute_amount()
invoice._compute_payments_widget_reconciled_info()
env.cr.commit()
def recompute_stock_fields(env):
"""Recompute stock quantities and valuations."""
Product = env['product.product']
products = Product.search([('type', '=', 'product')])
_logger.info("Recomputing stock for %d products", len(products))
# Trigger quantity computation
for product in products:
product._compute_quantities()
env.cr.commit()
def validate_user_access(env):
"""Validate user access rights after migration."""
User = env['res.users']
# Check for users with invalid group assignments
users = User.search([('active', '=', True)])
for user in users:
# Ensure base group assignments are valid
if not user.has_group('base.group_user'):
user.write({'groups_id': [(4, env.ref('base.group_user').id)]})
_logger.warning("Fixed missing base group for user: %s", user.login)
Phase 4: View Compatibility
Views are a common source of post-migration errors. When models and fields change, views that reference them will break. During migration, you need to review and update all custom views.
View Audit Script
Before migration, audit your custom views for potential issues:
-- Find all custom views (not from base Odoo)
SELECT v.id, v.name, v.model, v.type, md.module
FROM ir_ui_view v
JOIN ir_model_data md ON md.res_id = v.id
AND md.model = 'ir.ui.view'
WHERE md.module NOT IN (
'base', 'sale', 'purchase', 'account', 'stock',
'crm', 'hr', 'project', 'website', 'mail'
-- Add other standard Odoo modules
)
ORDER BY md.module, v.model;
-- Find views that reference potentially problematic fields
SELECT v.name, v.model, v.arch_db
FROM ir_ui_view v
WHERE v.arch_db LIKE '%field_name_that_changed%';
After migration, validate views render correctly:
# scripts/validate_views.py
def validate_views(env):
"""Validate all views render without errors."""
View = env['ir.ui.view']
views = View.search([('type', 'in', ['form', 'tree', 'kanban', 'search'])])
errors = []
for view in views:
try:
# Attempt to read the combined view
env[view.model].get_view(view.id, view.type)
except Exception as e:
errors.append({
'view_id': view.id,
'view_name': view.name,
'model': view.model,
'error': str(e)
})
return errors
Phase 5: Post-Migration Validation
The migration isn't complete until you've thoroughly tested every critical business workflow. This phase is where you catch issues before they impact production operations.
Workflow Testing Checklist
| Module | Critical Workflows to Test | Key Computed Fields |
|---|---|---|
| Sales | Quote creation, confirmation, invoicing | amount_total, invoice_status, delivery_status |
| Purchase | RFQ, PO confirmation, receipt | amount_total, invoice_status, receipt_status |
| Accounting | Invoice posting, payment, reconciliation | amount_residual, payment_state, invoice_date_due |
| Inventory | Transfers, adjustments, lot tracking | qty_available, qty_on_hand, valuation |
| CRM | Lead creation, opportunity stages, conversion | expected_revenue, probability, stage_id |
| HR | Employee records, leave requests, attendance | remaining_leaves, attendance_state |
Automated Actions & Scheduled Jobs
Verify that all automated actions and cron jobs function correctly after migration. These are often overlooked but can cause silent failures in background processes.
-- List all active scheduled actions
SELECT name, model_id, state, interval_number, interval_type, nextcall
FROM ir_cron
WHERE active = true
ORDER BY nextcall;
-- List all active automated actions
SELECT name, model_id, trigger, state
FROM base_automation
WHERE active = true
ORDER BY name;
Backup Strategy: Your Safety Net
Always create a complete backup of your database AND filestore before starting any migration. Test this backup on a separate server to ensure it can be restored successfully. This backup is your fail-safe if anything goes wrong.
# Create complete database backup
pg_dump -Fc -h localhost -U odoo -d production_db > backup_$(date +%Y%m%d_%H%M%S).dump
# Backup filestore
tar -czvf filestore_backup_$(date +%Y%m%d_%H%M%S).tar.gz /path/to/odoo/filestore/production_db
# Verify backup integrity
pg_restore --list backup_*.dump | head -20
# Test restore on staging server
pg_restore -h staging-server -U odoo -d staging_db backup_*.dump
Advanced: Multi-Company Migration Considerations
If you're operating a multi-company Odoo instance, migration complexity increases significantly. Each company may have unique customizations, and data integrity across companies must be maintained.
Company-Specific Sequences
Verify sequences (invoice numbers, SO numbers) maintain their numbering across companies
Inter-Company Rules
Test inter-company transaction rules still function correctly after migration
Chart of Accounts
Ensure each company's chart of accounts and fiscal positions are properly migrated
Summary: Migration Checklist
- Pre-Migration: Analyze models, fields, and data types
- Backup: Create and verify complete database + filestore backup
- Scripts: Develop pre-migration scripts for schema changes
- Execute: Run migration on staging environment first
- Compute: Populate all computed fields via post-migration scripts
- Views: Validate all views render correctly
- Workflows: Test critical business workflows in all modules
- Automations: Verify scheduled actions and cron jobs
- Access: Validate user permissions and group assignments
- Reports: Test critical reports and exports
Database migration in Odoo is not just about moving data—it's about restructuring and future-proofing your ERP system. With careful planning, a deep understanding of Odoo's internal changes, and a clear process covering pre- and post-migration stages, you can ensure a smooth upgrade that minimizes risk and maximizes business value.
Frequently Asked Questions
Migration time varies significantly based on database size, number of custom modules, and complexity of customizations. A small database (under 10GB) with minimal customizations might complete in 2-4 hours. Larger enterprise databases with extensive customizations can take 1-3 days for the actual migration, plus additional time for testing. Always allocate at least twice the estimated time for unexpected issues.
While technically possible, skipping multiple versions is not recommended. Each version introduces changes that build upon the previous version. Skipping versions means you miss incremental migration scripts designed to handle specific changes. The safest approach is to migrate version by version (14→15→16→17→18), though this takes longer. Some migration service providers offer direct multi-version jumps with custom scripts.
Custom modules require individual attention during migration. They won't automatically work in the new version because they depend on specific API methods, field structures, and framework behaviors that may have changed. Each custom module must be analyzed for deprecated methods, updated field references, and framework compatibility. Plan for custom module updates as a separate phase of your migration project.
Both options are valid for migration. Odoo.sh offers built-in migration tools and automated upgrade assistance for standard modules, which can simplify the process. On-premise gives you more control over the migration timeline and custom scripts. For businesses with heavy customizations, on-premise migration often provides more flexibility. Consider your team's expertise, customization level, and long-term hosting preferences when deciding.
Third-party apps from the Odoo App Store may or may not be compatible with the new version. Check with each app developer for updated versions before migration. If an app isn't updated, you'll need to either wait for the developer to release a compatible version, find an alternative app that provides similar functionality, or commission a custom update. Never assume third-party apps will work without verification.
