All posts
Developer12 min read

Migrating Odoo modules from 17 to 18: what breaks and how to fix it

A field-by-field breakdown of the ORM changes, view syntax shifts, and JavaScript regressions that hit most custom modules when upgrading from Odoo 17 to 18.

Odoo 18 shipped in October 2024. By mid-2026, a large portion of active deployments are still on 17 - either because the upgrade budget hasn't been approved, or because the team has done a preliminary scan and found a long list of breakage to fix. This guide is for developers doing that scan and the subsequent remediation.

What changed at the ORM level#

_compute_display_name replaces name_get#

name_get() was deprecated in 16 and removed in 18. Every model that overrode it for display formatting must switch to _compute_display_name.

Odoo 17:

python
def name_get(self):
    return [(rec.id, f"{rec.code} - {rec.name}") for rec in self]

Odoo 18:

python
def _compute_display_name(self):
    for rec in self:
        rec.display_name = f"{rec.code} - {rec.name}"

The display_name field is now a regular stored computed field with a depends decorator. Add @api.depends('code', 'name') to ensure recomputation. If your module also overrode name_search, that method is still valid for 18 - keep it.

Fields.Html sanitization tightened#

fields.Html in 18 applies stricter sanitisation by default. The sanitize parameter defaults to True and the allowed tag set is narrower. Custom modules that stored raw HTML (custom report templates, rich-text descriptions) and relied on sanitize=False must either keep that parameter explicit or move the content to fields.Text.

ir.actions.act_window view_mode "list" replaces "tree"#

The tree view type was an alias for list since 16. In 18 the alias is removed from view_mode strings. Any ir.actions.act_window that uses view_mode = 'tree,kanban' must be updated to view_mode = 'list,kanban'. This affects both Python code and XML data files.

Search across your module: grep -r "view_mode.tree" --include=".xml" --include="*.py".

AbstractModel._auto removed#

AbstractModel._auto = False was used to suppress table creation. The correct approach in 18 is _abstract = True on the model class. Models inheriting models.AbstractModel already get this behaviour implicitly, but any model that combined models.Model with _auto = False (to create a SQL view model) must now use the models.Model class with _auto = False still - this case is unchanged. The specific removal is the undocumented pattern of setting _auto = False on abstract mixins.

View XML breaking changes#

attrs / states removed entirely#

If your module was still on the attrs / states syntax that was deprecated in 16, Odoo 18 raises a ValidationError at module install. The migration is mechanical:

Old (attrs):

xml
<field name="date_approve" attrs="{'invisible': [('state', '!=', 'approved')], 'required': [('state', '=', 'approved')]}"/>

New (18 syntax):

xml
<field name="date_approve" invisible="state != 'approved'" required="state == 'approved'"/>

The new syntax uses Python-like boolean expressions directly in the attribute. Relational traversal works too: invisible="partner_id.country_id.code != 'CA'".

Run grep -rn "attrs=" --include="*.xml" across your module directories before upgrading. Each hit is a required change.

statusbar_visible removed from statusbar widget#

The statusbar_visible attribute on is gone. Use invisible on the field plus a for stage filtering. Most uses of statusbar_visible were cosmetic; verify whether the restriction still applies to your workflow.

notebook pages and group modifiers#

The col attribute on tags is removed. Odoo 18 uses CSS grid exclusively for form layout. Groups render as two-column by default. Custom CSS that relied on fixed column counts needs updating. The colspan attribute on fields inside groups is also dropped.

JavaScript and OWL changes#

Legacy client actions (AbstractAction) removed#

If your module registered a client action using the legacy web.AbstractAction widget, it does not load in 18. The migration path is to convert the action to an OWL component registered with the action manager:

javascript
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";

class MyClientAction extends Component {
    static template = "my_module.MyClientAction";
}

registry.category("actions").add("my_module.my_action", MyClientAction);

Widget registration via registry#

Custom field widgets registered with the legacy fieldRegistry are broken. In 18, register using:

javascript
import { registry } from "@web/core/registry";
registry.category("fields").add("my_widget", MyFieldWidget);

The widget class must extend standardFieldProps and use useComponent for owl reactivity. Each field type (many2one, char, etc.) has a specific base class in @web/views/fields/.

RPC calls: jsonrpc removed#

The jsonrpc helper from the legacy web client is removed. Use the rpc service from @web/core/network/rpc:

javascript
import { useService } from "@web/core/utils/hooks";

setup() {
    this.rpc = useService("rpc");
}

async fetchData() {
    const result = await this.rpc("/my_module/endpoint", { param: value });
}

Manifest changes#

'version' key convention#

Odoo 18 expects the version in __manifest__.py to follow the 18.0.x.y.z convention. While this is not strictly enforced, OCA linters and the App Store reject modules that don't follow it. Update all manifests: 'version': '18.0.1.0.0'.

'auto_install' on dependency arrays#

If you used 'auto_install': True with specific dependency conditions, verify against 18's module loading order - the resolution algorithm changed slightly in how it handles optional dependencies.

Data file migration#

ir.sequence code changes#

If your module defines ir.sequence records and references them by code in Python, verify the codes haven't changed in base. Odoo 18 renamed several internal sequence codes in the accounting and stock modules.

CSV access rights#

The model external ID format for access rights shifted slightly: the prefix model_ is still used but Odoo 18 is stricter about matching the Python class name exactly. A class named SaleOrderLine must appear as model_sale_order_line in the ir.model.access.csv id column. Mismatches that worked by accident in 17 may fail on install in 18.

Migration checklist#

Before testing on Odoo 18:

  • Replace all name_get with _compute_display_name
  • Update view_mode strings: treelist
  • Convert all attrs= to inline boolean expressions
  • Remove statusbar_visible attributes
  • Port legacy JS widgets and client actions to OWL
  • Replace jsonrpc calls with the rpc service
  • Remove col= from tags
  • Update manifest version to 18.0.x.y.z
  • Check fields.Html fields that stored unsanitised HTML

Run python odoo-bin --upgrade-path=/path/to/migration/scripts -d -u my_module with the Odoo 18 codebase to surface remaining issues. The upgrade log is your first diagnostic layer; uninstall errors are your second.


ERPeek lets you ask natural-language questions against an indexed copy of your Odoo 17 codebase, making it faster to identify which custom modules contain deprecated patterns before you start the 18 upgrade. See the pricing page for trial access.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free