"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.returnsis removed. It had no functional effect in recent versions; delete the decorator.- The
web_editormodule is gone. It was split intohtml_editorandhtml_builder. Any module with'depends': ['web_editor']fails to install. Repoint the dependency. - Internal
odoo.apiimports (Meta,Params,propagate) and the entireodoo/conf/package are removed. - The
get_module_resourcehelper is gone. Code that built file paths with it raisesImportErrorat module load; resolve paths withpathlibinstead. - On Enterprise, modules were renamed.
account_auto_transferbecameaccount_transfer,account_disallowed_expensesbecameaccount_fiscal_categories, and the wholehr_work_entry_contract_*family dropped itscontractinfix. 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._contextbecomeself.env.cr,self.env.uid,self.env.context.read_group()becomes_read_group()(note the argument rename:fieldsbecameaggregates).toggle_active()becomesaction_archive()oraction_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 becameIrUiView; the bus model's class becameBusBus. If you inherit by_inherit = 'ir.ui.view', which you should, the class name is irrelevant. - String
_inheritstill 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.