Quick Answer
Stop losing $40,000-$100,000 on broken upgrades. Wrong way: Modify core files, no x_ prefix, skip super(), fragile XPath. Result: 60+ hours fixing conflicts, data corruption, $18k in emergency fixes. Right way: Use _inherit, x_ prefix fields, call super(), stable XPath, migration scripts. Result: 4-hour painless upgrade, zero data loss, automatic migration.
The Upgrade Problem
You've built a custom module that extends res.partner with a loyalty points field. It's working great. Staff is using it daily.
Then Odoo releases version 19. You need to upgrade.
What happens next depends on whether you followed safe extension practices:
Scenario A (Did it wrong)
❌ Modified core Odoo files directly instead of creating a custom module
❌ Hardcoded field names instead of using proper inheritance
❌ Didn't follow naming conventions
Result: Upgrade breaks everything. Custom code conflicts with new version. You lose 60+ hours and $18,000 manually fixing broken modules. Data might even be corrupted.
Scenario B (Did it right)
✓ Used _inherit to extend models safely
✓ Followed naming conventions (x_ prefix for custom fields)
✓ Didn't override core methods without calling super()
✓ Created migration scripts for structural changes
Result: Upgrade goes smoothly. Modules auto-migrate. You're on new version in 4 hours with zero data loss.
We've implemented 150+ Odoo systems. The ones where developers followed safe extension practices? Upgrades are painless, sometimes automatic. The ones where developers "customized" haphazardly? We've seen upgrades cost $40,000-$100,000 in rework, data corruption recovery, and emergency consulting.
The Upgrade Reality
Odoo follows a "perpetual upgrade" model. There is NO backwards compatibility. You can only upgrade forward (version 17 → 18 → 19, never back to 17).
The Upgrade Process
1. Database dump taken from production
2. Sent to Odoo's upgrade server
3. Migration scripts run (Odoo's scripts transform your data)
4. Database restored to new version
5. Your custom modules must work with new version
Rule 1: NEVER Modify Core Odoo Files
The biggest mistake: Editing core Odoo code directly.
❌ WRONG
# WRONG - Do NOT do this
/opt/odoo/addons/sale/models/sale.py # Modifying core file!
# In sale.py, you add a custom field:
class SaleOrder(models.Model):
_name = 'sale.order'
custom_field = fields.Char() # You added this directly to core file
Why this breaks: When Odoo upgrades, it replaces /opt/odoo/addons/sale/ with the new version. Your changes are GONE. Poof.
✓ RIGHT
# RIGHT - Create a custom module
/opt/odoo/addons/custom_sales/models/sale_extension.py
class SaleOrder(models.Model):
_inherit = 'sale.order' # Extend, don't modify
custom_field = fields.Char() # Added via inheritance
Now your custom module is separate. Upgrades don't touch it.
Rule 2: Use the x_ Prefix for Custom Fields
Why: Prevents naming conflicts with future Odoo updates.
✓ Correct
class SaleOrder(models.Model):
_inherit = 'sale.order'
x_custom_note = fields.Char() # x_ prefix = custom field
x_is_priority = fields.Boolean() # Always prefixed
x_estimated_arrival = fields.Date()
❌ Wrong
class SaleOrder(models.Model):
_inherit = 'sale.order'
custom_note = fields.Char() # No x_ prefix, could conflict
is_priority = fields.Boolean() # Dangerous, Odoo might add this
Real scenario: Odoo version 18 adds a new is_priority field to sale.order. Your custom module has its own is_priority field with different meaning. Conflict. Data corruption. If you'd used x_is_priority, zero conflict.
Rule 3: Always Call super() When Overriding Methods
❌ WRONG - Missing super()
# WRONG - Missing super()
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
"""Confirm the order."""
print("Order confirmed")
return True
When you call order.action_confirm(), only print() runs. The actual Odoo confirmation logic DOESN'T run. Order isn't really confirmed. Workflows break. Data is inconsistent.
✓ RIGHT - Calls super()
# RIGHT - Calls super()
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
"""Confirm the order and do custom logic."""
# Call parent method first
result = super().action_confirm()
# Then do custom logic
print("Order confirmed")
self.x_custom_flag = True
return result
Why it matters for upgrades: When Odoo updates action_confirm() logic, your method automatically gets the improvements (because super() calls it). If you don't call super(), you're locked into old logic. Upgrade breaks.
Rule 4: Don't Redefine Fields, Add Attributes Instead
❌ WRONG - Redefining Field
# WRONG - Redefining field
class SaleOrder(models.Model):
_inherit = 'sale.order'
# Trying to make order_date required (it already exists)
order_date = fields.Datetime(required=True) # Redefining!
This creates a second definition of order_date. Database gets confused. Data corrupts during upgrade.
✓ SAFEST - Use Validation
# SAFEST - Use a computed field or validation
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.constrains('order_date')
def _check_order_date(self):
"""Validate that order_date is set."""
for record in self:
if not record.order_date:
raise ValidationError("Order date is required")
Now you're not redefining, just adding validation. Safe for upgrades.
Rule 5: Use Stable XPath for View Inheritance
❌ WRONG - Fragile XPath
<!-- WRONG - Fragile xpath -->
<xpath expr="//div[1]/group[1]/field[3]" position="after">
<field name="x_custom_field"/>
</xpath>
This says: "In the first div, first group, third field, add my field after." If Odoo's new version adds a field BEFORE that third field, your xpath breaks. View won't load.
✓ RIGHT - Stable XPath
<!-- RIGHT - Stable xpath using unique attributes -->
<xpath expr="//field[@name='order_date']" position="after">
<field name="x_custom_field"/>
</xpath>
This says: "After the field named 'order_date', add my field." Even if Odoo reorganizes the view, this works because it's based on WHAT exists, not WHERE it is.
Rule 6: Create Migration Scripts for Structural Changes
When needed: If you change field types, remove fields, or need to transform data during upgrade.
Migration Script Location
custom_module/
├── models/
├── views/
├── migrations/
│ ├── 18.0.1.0.0/
│ │ └── pre-migration.py
│ ├── 19.0.1.0.0/
│ │ ├── pre-migration.py
│ │ ├── post-migration.py
│ │ └── my-migration.py
Example Migration Scripts
def migrate(cr, version):
"""
Migration script for moving from version 18 to 19.
Runs BEFORE module code is updated.
"""
# Backup old field data before changing type
cr.execute("""
ALTER TABLE sale_order
ADD COLUMN x_priority_old VARCHAR;
""")
cr.execute("""
UPDATE sale_order
SET x_priority_old = x_priority_number;
""")
def migrate(cr, version):
"""
Runs AFTER module code is updated.
Transform data from old format to new.
"""
# Convert old field to new format
cr.execute("""
UPDATE sale_order
SET x_priority = CASE
WHEN x_priority_old = '1' THEN 'low'
WHEN x_priority_old = '2' THEN 'medium'
WHEN x_priority_old = '3' THEN 'high'
ELSE 'low'
END;
""")
# Drop old field
cr.execute("""
ALTER TABLE sale_order
DROP COLUMN x_priority_old;
""")
Rule 7: Update Module Version in Manifest
# Wrong - Same version for 17, 18, 19
{
'name': 'Custom Sales',
'version': '1.0.0', # Not version-specific!
}
# Right - Version-specific
{
'name': 'Custom Sales',
'version': '19.0.1.0.0', # 19 = Odoo 19, 1.0.0 = your version
'depends': ['sale'],
}
Complete Pre-Upgrade Checklist
6 Weeks Before Upgrade
❏ Review all custom modules
❏ Check for modifications to core files (REMOVE if found)
❏ Audit custom fields - all prefixed with x_?
❏ Audit method overrides - all call super()?
❏ Audit view xpaths - all using stable attributes?
4 Weeks Before Upgrade
❏ Set up staging database (copy of production)
❏ Test custom modules on staging with new Odoo version
❏ Create migration scripts for structural changes
❏ Update manifest version numbers
❏ Test migration scripts on staging
Frequently Asked Questions
Why must custom fields start with x_ prefix?
The x_ prefix prevents naming conflicts with future Odoo updates. If you add is_priority and Odoo v18 adds the same field name with different meaning, data corrupts. With x_is_priority, zero conflict—Odoo never uses x_ prefix for core fields.
What happens if I don't call super() when overriding methods?
Parent method logic doesn't execute. For action_confirm(), order won't actually confirm—workflows break, data inconsistent. When Odoo upgrades and improves that method, you don't get improvements. Your code is locked to old logic, causing upgrade failures.
When do I need migration scripts?
Use migration scripts when: (1) Changing field types (Selection to Char), (2) Removing fields, (3) Transforming data between versions. pre-migration.py runs before module code updates (backup data). post-migration.py runs after (transform data).
How much does a broken upgrade cost vs. doing it right?
Done wrong: 60+ hours fixing conflicts, $18,000-$40,000 in emergency consulting, potential data corruption. Done right: 4-hour painless upgrade, zero data loss, automatic migration. Following safe extension practices saves $40,000-$100,000.
Free Pre-Upgrade Compatibility Audit
Stop worrying about whether your upgrades will break. We'll scan all custom modules for unsafe extension patterns, identify code that will break during upgrade, audit field naming conventions, review method overrides, create a migration plan with migration scripts, and test on staging environment. Most D2C brands discover their custom code isn't upgrade-safe only when trying to upgrade. That's when problems cost $40,000-$100,000 in emergency fixes. Knowing your code is safe upfront saves that entire cost.
