Odoo 16 to 20 migration: a guide for custom modules
A large, multi-app release where most of the risk for custom modules sits in payment integrations, the ORM binary-field internals, the web export/RPC layer, and the many private controller endpoints that have been reworked.
Treat this as a big jump rather than a routine bump: rather than chasing every change at once, start by getting your module to install cleanly against a fresh Odoo 20 database, then walk your integration points one workflow at a time. The safest first practical step is to grep your codebase for direct references to Odoo controllers and low-level ORM helpers — those are the things most likely to have moved or vanished under you.
The areas most likely to bite are payment provider integrations (several public JSON endpoints are gone), anything that touches binary/attachment fields at the ORM level, and frontend/RPC code that leaned on the old DataSet or export routes. Note that this guide currently tracks the Odoo 20 development branch, which is still pre-release, so expect it to be refreshed once 20 reaches general availability.
What needs the closest look
Adyen provider info endpoint is gone
Removed method — AdyenController.adyen_provider_info
This was the public JSON route the Adyen checkout used to fetch the provider state and client key before starting a transaction. It has been removed, so if your module calls /payment/adyen/provider_info or overrides adyen_provider_info, that path no longer exists — you'll need to move to whatever provider bootstrapping the new payment flow uses.
Removed in Odoo 20 — it was defined as:
@http.route('/payment/adyen/provider_info', type='json', auth='public')
def adyen_provider_info(self, provider_id):
Authorize.Net provider info endpoint is gone
Removed method — AuthorizeController.authorize_get_provider_info
Same story as Adyen: the public route that returned the provider state, login ID and client key for Authorize.Net has been dropped. Any custom checkout code or JS that hit /payment/authorize/get_provider_info must be reworked around the redesigned payment initialization.
Removed in Odoo 20 — it was defined as:
@http.route('/payment/authorize/get_provider_info', type='json', auth='public')
def authorize_get_provider_info(self, provider_id):
Binary fields no longer declare their own column type
Removed method — Binary.column_type
This internal property told the ORM how a binary field maps to a database column (attachment vs. bytea). It's been removed as part of reworking how binary storage is handled. If your module subclasses the Binary field or relied on this to decide storage behaviour, you'll need to revisit against the new field internals.
Removed in Odoo 20 — it was defined as:
@property
def column_type(self):
Binary field bin_size computation reworked
Removed method — Binary.compute_value
This handled the special case where a binary field is read as a human-readable size instead of its full contents, keeping the bin_size cache in sync. With it removed, any custom binary field that depended on this bin_size caching trick needs re-testing to confirm size vs. full-content reads still behave as you expect.
Removed in Odoo 20 — it was defined as:
def compute_value(self, records):
CSV export handler renamed and error payload cleaned up
Renamed method — CSVExport.web_export_csv
The controller method behind /web/export/csv was renamed from the generic index to web_export_csv, and it now uses the newer route/serialize_exception helpers with a corrected error code. The URL itself is unchanged, so if you only call the route you're fine; if you override or extend the handler method, rename your override and update the imports it relies on.
Before — Odoo 16
@http.route('/web/export/csv', type='http', auth="user")
def index(self, data):
try:
return self.base(data)
except Exception as exc:
_logger.exception("Exception during request handling.")
payload = json.dumps({
'code': 200,
'message': "Odoo Server Error",
'data': http.serialize_exception(exc)
})
raise InternalServerError(payload) from exc
After — Odoo 20
@route('/web/export/csv', type='http', auth='user')
def web_export_csv(self, data):
try:
return self.base(data)
except Exception as exc:
_logger.exception("Exception during request handling.")
payload = json.dumps({
'code': 0,
'message': "Odoo Server Error",
'data': serialize_exception(exc)
})
raise InternalServerError(payload) from exc
Deprecated serialized_cursor removed
Removed method — Connection.serialized_cursor
This was a long-deprecated shim that just forwarded to the normal cursor. It's now gone entirely, so anywhere your module still calls serialized_cursor must switch to the standard cursor method.
Removed in Odoo 20 — it was defined as:
def serialized_cursor(self, **kwargs):
Manual cursor autocommit helper removed
Removed method — Cursor.autocommit
The old autocommit(on) method for toggling a cursor's isolation level has been dropped after being deprecated. If your module managed transactions this way, set the connection's autocommit or isolation level directly on the underlying connection instead.
Removed in Odoo 20 — it was defined as:
def autocommit(self, on):
Also worth checking
The bulk of the remaining work clusters by app and by layer, and most of it is a quick pass rather than a deep rewrite.
- Payments deserve the closest look. Beyond the Adyen and Authorize endpoints above, the pattern is clear: public provider-info routes are being consolidated, so audit every payment acquirer integration you maintain end to end rather than patching one route at a time.
- ORM / database core. Alongside the binary-field changes, several low-level helpers on the Binary field (its cache, column and record conversion routines) and on the cursor (the helper that split large
INconditions) have been removed. These only matter if you subclass fields or hand-build SQL, but if you do, review those spots carefully. - Portal (Website / Sales / Accounting frontend). The customer portal lost its attachment-upload, address-form validation and post-account-update hooks. If your module extends the customer portal — custom document uploads, extra address fields, or logic that ran when a customer edited their details — those override points are gone and need to be re-hooked into the new portal flow.
- Web / RPC frontend. The old DataSet layer (its
call,loadandresequenceoperations) and the/web/export/csvhandler have moved on. Modern JS should already use ORM services, so this is mostly a quick pass unless you have legacy client code. - Point of Sale / IoT. The customer-display controller (its display, refresh and serialized-order routes) and the IoT driver's certificate check have been removed. If you customise the POS customer display or IoT box drivers, expect to rewire against the new endpoints.
Beyond these, roughly 34,704 smaller internal changes exist — compute helpers and body-only edits — only relevant if you override Odoo's private helpers.
Your migration checklist
- Install your module against a clean Odoo 20 database and fix whatever prevents it from loading before touching business logic.
- Audit every payment provider integration first — replace calls to the removed Adyen and Authorize provider-info routes and confirm your checkout still initializes.
- Search for and update any low-level ORM usage: deprecated cursor helpers (serialized_cursor, autocommit) and custom Binary field subclasses.
- Review customer portal extensions — re-hook attachment uploads, address validation and account-update logic into the new portal flow.
- Rename any override of the CSV export handler to web_export_csv and refresh legacy JS that used DataSet, then re-test POS customer display and IoT driver customizations.
- Run your full test suite and manual smoke tests per workflow, and plan to re-verify once Odoo 20 reaches general availability.