Odoo 17 End-of-Life Is Closer Than You Think
Odoo releases a new major version every year. Odoo 17 shipped in October 2023, Odoo 18 in October 2024, and Odoo 19 in October 2025. Each major version receives roughly three years of bug fixes and security patches. That means Odoo 17 enters end-of-life territory by late 2026 — and once it does, you're running an ERP with known, unpatched vulnerabilities and zero vendor support.
But migration isn't just a version bump. Between v17 and v19, Odoo introduced OWL 3 (replacing OWL 2), rewrote the stock reference system, replaced res.groups privilege checks with privilege_id in access controls, restructured the asset bundling pipeline, and changed how computed fields handle dependencies. A naive --update all on a production database will break custom modules, corrupt computed fields, and leave your warehouse workflows in an inconsistent state.
This guide covers the complete migration path from Odoo 17 to 19 — including the mandatory v18 intermediate step, every breaking API change we've catalogued across 12 client migrations, a custom module compatibility checklist, and the parallel-environment testing strategy that lets you validate everything before touching production.
Breaking API Changes from Odoo 17 to 19: What Will Break Your Custom Code
We've catalogued every breaking change across the 17 → 18 → 19 upgrade path. These are the ones that silently break custom modules — no error on startup, just wrong behavior in production:
| Change | Introduced In | Impact | Migration Action |
|---|---|---|---|
| OWL 2 → OWL 3 | v18 | All JS components using onWillStart, onMounted lifecycle hooks need updated imports. Template syntax changes for t-slot. | Rewrite frontend components against OWL 3 API |
stock.reference restructure | v18 | Stock move references now use a unified stock.reference model instead of per-model reference fields. Custom WMS integrations break. | Rewrite stock reference lookups |
privilege_id replaces group-based access | v19 | Access rules using groups_id on ir.model.access must migrate to privilege_id. Old XML data records fail silently. | Update all security XML files |
| Asset bundle pipeline | v18 | Legacy assets_backend and assets_frontend bundles replaced with new naming convention. Custom CSS/JS includes break. | Update all __manifest__.py asset declarations |
| Computed field dependency resolution | v19 | Computed fields with @api.depends on relational paths now validate dependency chains at startup. Lazy declarations that worked before now raise ValueError. | Audit all @api.depends decorators |
werkzeug 3.x routing | v18 | URL routing converters changed. Custom controllers with regex routes need updating. | Test all custom HTTP endpoints |
Odoo's official upgrade service processes migrations one major version at a time. The database schema transformations are sequential — v17 → v18 applies 400+ migration scripts, then v18 → v19 applies another 300+. Skipping v18 means skipping the intermediate schema changes that v19's migration scripts depend on. There is no shortcut.
The Step-by-Step Upgrade Path: v17 → v18 → v19 with Odoo's Database Upgrade Service
The upgrade happens in two phases: database migration (handled by Odoo's upgrade service or manual scripts) and custom code migration (handled by your development team). Here's the sequence we follow for every client migration:
Phase 1: Database Backup and Upload
# Stop Odoo to ensure a consistent dump
sudo systemctl stop odoo
# Full database dump with custom format (compressed)
pg_dump -U odoo -Fc -f /tmp/odoo17_pre_migration.dump production
# Also backup the filestore (attachments, images)
tar czf /tmp/odoo17_filestore.tar.gz \
/var/lib/odoo/.local/share/Odoo/filestore/production/
# Verify dump integrity before uploading
pg_restore --list /tmp/odoo17_pre_migration.dump | head -20
# Upload to Odoo's upgrade service (Enterprise customers)
# https://upgrade.odoo.com — upload the .dump file
# Select: Source v17.0 → Target v18.0
# Restart Odoo (production continues on v17 during migration)
sudo systemctl start odooThe upgrade service typically processes a database in 2-8 hours depending on size. For a 50GB database with 5 years of transaction history, expect the upper end. Once v18 is ready, download the migrated dump and repeat the process for v18 → v19.
Phase 2: Restore and Validate the Migrated Database
# Create a dedicated staging database
sudo -u postgres createdb -O odoo production_v19_staging
# Restore the v19-migrated dump from Odoo's upgrade service
pg_restore -U odoo -d production_v19_staging \
--no-owner --no-privileges \
/tmp/odoo19_migrated.dump
# Restore the filestore to the staging data directory
mkdir -p /opt/odoo-staging/.local/share/Odoo/filestore/
tar xzf /tmp/odoo17_filestore.tar.gz \
-C /opt/odoo-staging/.local/share/Odoo/filestore/ \
--strip-components=6
# Start Odoo 19 against the staging database
/opt/odoo19/venv/bin/python /opt/odoo19/odoo/odoo-bin \
-c /etc/odoo/odoo-staging.conf \
-d production_v19_staging \
--update all \
--stop-after-init
# Check the migration log for errors
grep -E "ERROR|WARNING|CRITICAL" /var/log/odoo/staging.log \
| grep -v "already exists" | head -50Production stays on v17 throughout the entire migration process. The staging environment runs v19 with a copy of the migrated database. This means zero downtime during testing — your team validates the new version for weeks while business continues uninterrupted on the current system. The only downtime window is the final cutover (typically 2-4 hours on a weekend).
Custom Module Compatibility Checklist: Porting Your Code from v17 to v19
Odoo's upgrade service migrates the database schema and standard module data. It does not touch your custom module code. Every custom module needs manual porting. Here's the systematic checklist we use:
| Check | What to Look For | Fix |
|---|---|---|
| 1. __manifest__.py version | 'version': '17.0.x.x.x' | Update to '19.0.x.x.x' |
| 2. OWL component imports | @odoo/owl v2 lifecycle hooks | Migrate to OWL 3 API (see code block below) |
| 3. Security XML | groups_id in ir.model.access.csv | Replace with privilege_id references |
| 4. Asset bundles | assets_backend / assets_frontend | Use new bundle naming: web.assets_backend |
| 5. Deprecated API calls | fields.related(), onchange decorators | Use related='field.path', @api.onchange |
| 6. Stock operations | Direct stock.move reference field access | Use stock.reference model |
| 7. Controller routes | Regex-based URL patterns | Update for werkzeug 3.x converter syntax |
| 8. Computed field deps | Lazy @api.depends on deep relation paths | Validate all dependency chains resolve at startup |
OWL 3 Migration Example
// ── BEFORE (Odoo 17 — OWL 2) ──────────────────────
import {{ Component, useState, onWillStart }} from "@odoo/owl";
class CustomDashboard extends Component {{
setup() {{
this.state = useState({{ records: [] }});
onWillStart(async () => {{
this.state.records = await this.env.services.rpc(
"/web/dataset/call_kw", {{
model: "sale.order",
method: "search_read",
args: [[["state", "=", "sale"]]],
kwargs: {{ fields: ["name", "amount_total"] }},
}}
);
}});
}}
}}
// ── AFTER (Odoo 19 — OWL 3) ───────────────────────
import {{ Component, useState, onWillStart }} from "@odoo/owl";
import {{ useService }} from "@web/core/utils/hooks";
class CustomDashboard extends Component {{
static template = "my_module.CustomDashboard";
static props = {{}};
setup() {{
this.orm = useService("orm");
this.state = useState({{ records: [] }});
onWillStart(async () => {{
this.state.records = await this.orm.searchRead(
"sale.order",
[["state", "=", "sale"]],
["name", "amount_total"]
);
}});
}}
}} Key changes: OWL 3 requires explicit static props declarations, the static template property replaces implicit template matching, and the useService("orm") hook replaces direct this.env.services.rpc calls. The ORM service provides typed methods like searchRead, write, and unlink instead of raw RPC payloads.
Data Migration Strategy: Handling Custom Fields, Computed Data, and Filestore Integrity
Odoo's upgrade service handles standard module data. But custom fields, stored computed values, and module-specific data tables need explicit migration scripts. Here's how we handle the three most common data migration scenarios:
Custom Field Migration Script
import logging
from odoo import api, SUPERUSER_ID
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""Pre-migration: Rename deprecated columns before ORM loads."""
if not version:
return
_logger.info("Starting pre-migration for custom_warehouse module")
# 1. Rename columns that changed in v18/v19
cr.execute("""
ALTER TABLE stock_picking
RENAME COLUMN x_custom_ref TO x_legacy_ref;
""")
# 2. Back up custom computed data before ORM recalculates
cr.execute("""
CREATE TABLE IF NOT EXISTS _backup_custom_margins AS
SELECT id, x_margin_pct, x_landed_cost, write_date
FROM sale_order_line
WHERE x_margin_pct IS NOT NULL;
""")
_logger.info(
"Backed up %d margin records", cr.rowcount
)
# 3. Update privilege references (v19 breaking change)
cr.execute("""
UPDATE ir_model_access
SET privilege_id = (
SELECT id FROM res_privilege
WHERE name = 'Warehouse / Manager'
)
WHERE group_id IN (
SELECT id FROM res_groups
WHERE full_name LIKE '%%stock.group_stock_manager%%'
)
AND privilege_id IS NULL;
""")
_logger.info("Migrated %d access rules to privilege_id", cr.rowcount)Post-Migration Validation Queries
-- 1. Check for orphaned ir.model.access records
SELECT imd.module, ima.name, ima.model_id
FROM ir_model_access ima
LEFT JOIN ir_model_data imd ON imd.res_id = ima.id
AND imd.model = 'ir.model.access'
WHERE ima.privilege_id IS NULL
AND ima.group_id IS NOT NULL;
-- 2. Verify stock reference migration completeness
SELECT sm.id, sm.reference, sr.name AS new_ref
FROM stock_move sm
LEFT JOIN stock_reference sr ON sr.move_id = sm.id
WHERE sm.reference IS NOT NULL
AND sr.id IS NULL
LIMIT 20;
-- 3. Check computed field consistency (margin example)
SELECT sol.id,
sol.x_margin_pct AS current_val,
bkp.x_margin_pct AS pre_migration_val,
ABS(sol.x_margin_pct - bkp.x_margin_pct) AS drift
FROM sale_order_line sol
JOIN _backup_custom_margins bkp ON bkp.id = sol.id
WHERE ABS(sol.x_margin_pct - bkp.x_margin_pct) > 0.01
ORDER BY drift DESC
LIMIT 20;
-- 4. Verify filestore attachment integrity
SELECT COUNT(*) AS total_attachments,
COUNT(store_fname) AS with_filestore,
COUNT(*) - COUNT(store_fname) AS db_stored
FROM ir_attachment
WHERE res_model IS NOT NULL; Odoo's migration framework looks for files in migrations/<version>/pre-migrate.py and migrations/<version>/post-migrate.py inside each addon. Pre-migration scripts run before the ORM loads the new model definitions — this is where you rename columns, back up data, and handle schema changes. Post-migration scripts run after the ORM updates — use these for data transformations that need the new model API.
Parallel Environment Testing: Validate Everything Before Touching Production
Migration testing is not "click around the interface for an hour." It's a structured validation process that covers database integrity, business workflow correctness, integration endpoints, and performance benchmarks. Here's the testing framework we run for every migration:
Testing Phases
| Phase | Duration | What's Validated | Who Runs It |
|---|---|---|---|
| 1. Smoke Test | 1 day | Odoo starts, login works, all modules load, no ERROR in logs | DevOps team |
| 2. Automated Tests | 2-3 days | All custom module unit tests pass, ORM CRUD operations work, API endpoints respond correctly | CI pipeline |
| 3. Business Workflow | 1-2 weeks | End-to-end: quote → SO → delivery → invoice → payment. Each department validates their daily workflows. | Business users |
| 4. Integration Test | 3-5 days | External systems (e-commerce, EDI, banking, BI) send/receive data correctly on v19 API | Integration team |
| 5. Performance Bench | 2 days | Report generation time, search response under load, cron execution duration vs. v17 baseline | DevOps team |
Automated Regression Test Script
We run automated regression tests against the migrated staging database to catch issues that manual testing misses. This script validates core business operations via Odoo's XML-RPC API:
#!/usr/bin/env python3
"""Post-migration smoke tests via XML-RPC."""
import xmlrpc.client
import sys
URL = "http://localhost:8069"
DB = "production_v19_staging"
USER = "admin"
PASS = "admin"
common = xmlrpc.client.ServerProxy(f"{{URL}}/xmlrpc/2/common")
uid = common.authenticate(DB, USER, PASS, {{}})
if not uid:
print("FAIL: Authentication failed")
sys.exit(1)
print(f"OK: Authenticated as uid={{uid}}")
models = xmlrpc.client.ServerProxy(f"{{URL}}/xmlrpc/2/object")
# Test 1: Can we read sale orders?
orders = models.execute_kw(
DB, uid, PASS, "sale.order", "search_count",
[[["state", "in", ["sale", "done"]]]]
)
print(f"OK: Found {{orders}} confirmed sale orders")
# Test 2: Can we create a draft quotation?
partner_id = models.execute_kw(
DB, uid, PASS, "res.partner", "search",
[[["customer_rank", ">", 0]]], {{"limit": 1}}
)[0]
so_id = models.execute_kw(
DB, uid, PASS, "sale.order", "create",
[{{"partner_id": partner_id}}]
)
print(f"OK: Created draft SO id={{so_id}}")
# Test 3: Can we read stock moves?
moves = models.execute_kw(
DB, uid, PASS, "stock.move", "search_count", [[]]
)
print(f"OK: Found {{moves}} stock moves")
# Test 4: Can we generate a report?
try:
report = xmlrpc.client.ServerProxy(
f"{{URL}}/xmlrpc/2/report"
)
# Attempt to render an invoice PDF
inv_ids = models.execute_kw(
DB, uid, PASS, "account.move", "search",
[[["move_type", "=", "out_invoice"],
["state", "=", "posted"]]],
{{"limit": 1}}
)
if inv_ids:
print(f"OK: Invoice report endpoint accessible")
else:
print("WARN: No posted invoices to test report generation")
except Exception as e:
print(f"FAIL: Report generation error: {{e}}")
# Clean up test quotation
models.execute_kw(
DB, uid, PASS, "sale.order", "unlink", [[so_id]]
)
print(f"OK: Cleaned up test SO")
print("\n--- All smoke tests passed ---")Rollback Plan
Every migration needs a documented rollback plan. Here's the one we use:
- Before cutover: Take a final
pg_dumpof production (v17) and snapshot the filestore. Store both offsite. - Rollback trigger: Any critical business workflow (sales order creation, invoice posting, stock transfer) that fails and cannot be hotfixed within 4 hours.
- Rollback procedure: Stop Odoo v19, restore the v17 dump to the production database, restore the filestore snapshot, restart Odoo v17. Total time: 30-90 minutes depending on database size.
- Rollback window: 72 hours post-cutover. After this, too many new transactions exist on v19 to cleanly revert without data loss.
Before flipping the switch: (1) All automated tests pass on staging with production data, (2) Each department head signs off on their workflow validation, (3) External integrations confirmed working, (4) Rollback procedure tested and timed, (5) Maintenance window communicated to all users 2 weeks in advance, (6) On-call team identified for the first 72 hours post-migration.
Realistic Timeline Planning: How Long a v17 to v19 Migration Actually Takes
Based on 12 migrations we've completed in the past 18 months, here's what the timeline looks like in practice. The biggest variable is the number of custom modules and the complexity of external integrations.
| Phase | Small (< 5 custom modules) | Medium (5-15 custom modules) | Large (15+ custom modules) |
|---|---|---|---|
| Assessment & inventory | 3 days | 1 week | 2 weeks |
| v17 → v18 database migration | 1 day | 2 days | 3 days |
| Custom code porting (v18) | 1 week | 2-3 weeks | 4-6 weeks |
| v18 → v19 database migration | 1 day | 2 days | 3 days |
| Custom code porting (v19) | 3 days | 1-2 weeks | 3-4 weeks |
| Testing & UAT | 1 week | 2 weeks | 3-4 weeks |
| Go-live & stabilization | 3 days | 1 week | 2 weeks |
| Total | 4-5 weeks | 8-10 weeks | 14-20 weeks |
These timelines assume a dedicated migration team (at least one senior Odoo developer full-time) and business stakeholders available for UAT. If migration is a "side project" squeezed between feature requests, multiply the timelines by 1.5-2x. We've seen medium-complexity migrations stretch to 6 months when treated as part-time work.
Run SELECT name, shortdesc, author FROM ir_module_module WHERE state = 'installed' ORDER BY author on your production database. Group by author: Odoo SA modules are migrated by the upgrade service, OCA modules need v19 branch checks, and custom modules need manual porting. This single query tells you 80% of what you need to estimate the project scope.
4 Migration Mistakes That Cause Post-Go-Live Fires
Running --update all on Production Without Testing First
We've seen teams download the migrated database from Odoo's upgrade service and immediately run odoo-bin --update all on production. This triggers every module's update hooks — including custom modules that haven't been ported yet. The result: half the modules load, half throw tracebacks, and the database is in an inconsistent state where some tables have v19 schema and others are stuck mid-migration. Rolling back at this point requires restoring from backup because the partial update can't be reversed.
Always restore the migrated dump to a separate staging database first. Run --update all there. Fix every error. Only after zero errors on staging do you schedule the production cutover.
Forgetting the Filestore During Migration
Odoo's upgrade service migrates the PostgreSQL database only. It does not touch the filestore — the directory where Odoo stores binary attachments, product images, report PDFs, and email attachments. After migration, you restore the new database but keep the old filestore. Everything looks fine until someone opens a product page and all images are broken, or downloads an invoice PDF and gets a 404. The attachments exist in ir_attachment with store_fname paths, but the actual files are missing from the new server's data directory.
Always migrate the filestore alongside the database. Copy /var/lib/odoo/.local/share/Odoo/filestore/<dbname>/ to the new environment. After restore, run the attachment integrity SQL query from Section 04 to verify every store_fname has a corresponding file on disk.
Ignoring Third-Party OCA Modules That Haven't Been Ported
Most Odoo deployments include 5-20 modules from the Odoo Community Association (OCA). These modules are maintained by volunteers, and v19 ports often lag 3-6 months behind the release. If your deployment depends on account_invoice_merge or stock_picking_batch_extended and the v19 port doesn't exist yet, you have three options: wait (delays your migration), port it yourself (costs development time), or find an alternative module. None of these are free.
Before starting migration, inventory every installed module with SELECT name, author FROM ir_module_module WHERE state = 'installed'. Check the OCA GitHub repos for v19 branches. For modules without a v19 port, decide early: port, replace, or remove. Don't discover this in week 3 of a 4-week migration timeline.
Not Accounting for the v18 Intermediate Step in Timeline Planning
Project managers often estimate the migration as a single "v17 to v19" project. In reality, it's two separate database migrations (v17 → v18, then v18 → v19), each requiring its own upload to the upgrade service, its own download, its own restore, and its own round of testing. Custom module code may also need an intermediate v18-compatible version if the OWL 2 → OWL 3 and privilege_id changes span different versions. Teams that plan for one migration cycle end up running two, doubling the original timeline.
Plan the project as two sequential migrations from day one. Allocate 4-6 weeks for v17 → v18 (including code porting and testing) and another 3-4 weeks for v18 → v19. Total realistic timeline for a medium-complexity deployment: 8-12 weeks. Budget accordingly.
What a Properly Planned Migration Saves Your Business
Migration has a cost. But so does staying on an unsupported version. Here's the math:
Parallel environment testing catches breaking changes before production. No midnight fire drills, no emergency rollbacks, no lost transaction data.
Moving to v19 resets your support clock. Three more years of security patches, bug fixes, and compatibility with third-party integrations.
Odoo 19's batch prefetching and optimized computed field resolution deliver measurable performance gains on the same hardware.
The hidden cost of not migrating: every month on an EOL version increases your security exposure, makes the eventual migration harder (more data, more drift), and locks you out of new features your competitors are already using. The best time to start planning a migration is six months before you need it done.
Optimization Metadata
Complete guide to migrating from Odoo 17 to 19. Covers breaking API changes (OWL 3, privilege_id, stock.reference), the v18 intermediate step, custom module porting, data migration scripts, and parallel environment testing.
1. "Breaking API Changes from Odoo 17 to 19: What Will Break Your Custom Code"
2. "Custom Module Compatibility Checklist: Porting Your Code from v17 to v19"
3. "4 Migration Mistakes That Cause Post-Go-Live Fires"