The Odoo 18 to 19 Upgrade: What Actually Breaks

Most upgrade guides list new features. This one lists casualties: the specific things in custom code that stop working when you move from Odoo 18 to 19. They fall into three groups, ordered by how loudly they fail: boot-time crash, deprecation warning, or nothing at all.

"Will our customizations survive the jump to 19?" is the first question on every upgrade engagement. The honest answer is: most will, a few won't, and the few that won't are findable in advance. Here is the casualty list we work through, ordered by failure volume: what crashes the server, what only warns, and what looks alarming but is actually fine.

The headline change is structural, not behavioural

Odoo 19's biggest internal change is that the ORM was split apart. The old monolithic odoo/api.py, odoo/fields.py, and odoo/models.py are now a package: odoo/orm/. This sounds terrifying and mostly isn't. The public imports are all re-exported and backward-compatible. from odoo import api, fields, models and from odoo.fields import Char, Many2one work unchanged.

It only bites code that reached inside the framework: imports from internal locations like from odoo.api import Meta, Params, propagate. Those are gone with no replacement. That is the pattern for the whole upgrade. Public API is safe, private API is not.

What hard-breaks: the server won't boot

These stop a module loading entirely. Find them before you upgrade, not after.

  • @api.returns is removed. It had no functional effect in recent versions; delete the decorator.
  • The web_editor module is gone. It was split into html_editor and html_builder. Any module with 'depends': ['web_editor'] fails to install. Repoint the dependency.
  • Internal odoo.api imports (Meta, Params, propagate) and the entire odoo/conf/ package are removed.
  • The get_module_resource helper is gone. Code that built file paths with it raises ImportError at module load; resolve paths with pathlib instead.
  • On Enterprise, modules were renamed. account_auto_transfer became account_transfer, account_disallowed_expenses became account_fiscal_categories, and the whole hr_work_entry_contract_* family dropped its contract infix. A dependency on an old name will not resolve.

What soft-breaks: works now, warns loudly

These still function in 19 but emit deprecation warnings. They are the next major version's hard-breaks, so fix them during the upgrade while the code is already open:

  • self._cr, self._uid, self._context become self.env.cr, self.env.uid, self.env.context.
  • read_group() becomes _read_group() (note the argument rename: fields became aggregates).
  • toggle_active() becomes action_archive() or action_unarchive().
  • The <> and == domain operators become != and =.
  • check_field_access_rights() becomes _check_field_access().

What looks scary but is fine

Three things reliably cause upgrade panic and shouldn't:

  • XML view syntax is fully backward-compatible. No new mandatory tags, no attribute changes. Your view definitions carry over untouched.
  • Core class renames are cosmetic. ir.ui.view's Python class became IrUiView; the bus model's class became BusBus. If you inherit by _inherit = 'ir.ui.view', which you should, the class name is irrelevant.
  • String _inherit still works. v19 normalizes it to a list internally and the source now prefers list form, but a string declaration is not a breaking change.

New tools worth adopting, but not required

v19 adds a Domain class for composable domains, Constraint / Index / UniqueIndex table objects to replace _sql_constraints, and the @api.private / @api.readonly decorators. None of these are required for the upgrade. Treat them as a second pass, after the module boots and the warnings are clear.

The order to work through it

Boot first: fix every hard-break until the module installs on a v19 database. Then run a representative workload with deprecation warnings logged and clear them. Then, and only then, consider the optional modernizations. One environment note that bites teams late: v19 sets a minimum PostgreSQL of 13. An older database server is its own upgrade, and it is better discovered on day one than on cutover night.

"Will it survive?" is the wrong question. "Which of these specific things is in our code?" is the right one. Unlike the vague version, it has an answer you can produce with an afternoon of grep.