A large, foundational release where the framework's binary-field internals and several web/portal/discuss controller endpoints have shifted, so most custom-module work is about following renamed hooks and dropped routes rather than rewriting business logic.
Odoo 20 was released on 24 September 2026. This guide covers Odoo 17 → 20, Community Edition, built from the released version; Enterprise modules are not included in these counts. It counts 50,513 verified changes, including 9,557 in country localisation modules, which the per-area figures leave out.
Approach this one from the framework outward. Because the biggest structural changes sit deep in the ORM's binary-field handling and in the HTTP controllers that your frontend and portal code call, the safest first step is to get your module installing and its Python importing cleanly against a fresh Odoo 20 database, then chase down the endpoints and field hooks it actually touches. Don't try to fix everything at once — get it loading, then walk your integration points.
The areas most likely to bite are anything that overrides or extends binary/attachment field behaviour, any JavaScript or controller that hits Discuss, Calendar sync, IoT display, or customer-portal routes directly, and the account document ZIP download flow.
What needs the closest look
The 'download all as ZIP' route for accounting documents is gone
Removed method — AccountDocumentDownloadController.export_zip_documents
This was the endpoint that bundled a set of attachments into a single ZIP download from the accounting side. If your module links to that URL, exposes a 'download all' button, or overrides this controller, that path no longer exists — rework it to build the archive through the current attachment download mechanism or remove the feature.
Removed in Odoo 20 — it was defined as:
@http.route('/account/export_zip_documents', type='http', auth='user')
def export_zip_documents(self, **args):
Binary fields no longer declare their own database column type
Removed method — Binary.column_type
This internal hook told the ORM how a binary value maps to a storage column depending on whether it lives in an attachment. It has been removed as part of reworking how binary fields are stored. If you subclassed the binary field or relied on this to decide column typing, that logic must move to the new field internals — most custom modules only need to confirm they weren't overriding it.
Removed in Odoo 20 — it was defined as:
@property
def column_type(self):
The special 'bin_size' read path for binary fields was removed
Removed method — Binary.compute_value
This was the machinery that let you read a binary field as its size (bin_size) instead of its content and kept both cached correctly. Its removal signals that size-versus-content reading now works differently under the hood. If you have custom fields or code that leaned on the per-field bin_size context trick, verify that reading size still behaves as you expect.
Removed in Odoo 20 — it was defined as:
def compute_value(self, records):
The calendar credential-check endpoint has been dropped
Removed method — CalendarController.check_calendar_credentials
This route existed so calendar sync providers (Google, Microsoft) could override it and report whether the user's credentials were valid. It's gone. If you built a custom sync provider that overrode this endpoint, you'll need to hook into the current credential-checking flow instead of this route.
Removed in Odoo 20 — it was defined as:
@http.route('/calendar/check_credentials', type='json', auth='user')
def check_calendar_credentials(self):
The public Discuss channel-info endpoint is removed
Removed method — ChannelController.discuss_channel_info
This route returned the serialized information for a single Discuss channel to the frontend. As Discuss's data-loading moved to a different pattern, this endpoint was retired. Any custom JS or integration that fetched channel info from this URL should be repointed at the current channel-loading mechanism.
Removed in Odoo 20 — it was defined as:
@http.route("/discuss/channel/info", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_info(self, channel_id):
The 'mark channel as seen' endpoint is removed
Removed method — ChannelController.discuss_channel_mark_as_seen
This was the route the client called to record how far a user had read in a Discuss channel. It has been removed alongside the wider Discuss controller cleanup. If your module tracked read state or built custom chat UI against this URL, migrate to the current seen/acknowledge flow.
Removed in Odoo 20 — it was defined as:
@http.route("/discuss/channel/set_last_seen_message", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def discuss_channel_mark_as_seen(self, channel_id, last_message_id, allow_older=False):
The helper that chunked IDs for SQL IN clauses is gone from the cursor
Removed method — Cursor.split_for_in_conditions
This utility split a big list of record IDs into safe-sized tuples for use in IN conditions. It has been removed from the database cursor. If any of your low-level query code called it on the cursor, switch to building batches yourself or use the general splitting utilities rather than expecting it on the connection object.
Removed in Odoo 20 — it was defined as:
def split_for_in_conditions(self, ids, size=None):
Also worth checking
The rest of the verified work clusters predictably by app, and most of it is a quick pass rather than a deep rewrite.
- Framework / ORM (close look if you touch storage): Beyond the binary-field hooks called out above, several other binary-field conversion helpers — the ones that turned a value into its cached form, its column form, and back into a record value — were removed as part of the same storage rework. Only relevant if you subclassed binary fields or wrote custom field types; ordinary modules are unaffected.
- Discuss / messaging (close look for chat integrations): The controller cleanup that removed the channel-info and mark-as-seen routes also dropped the endpoints for fetching a channel's members, loading its messages, and muting it. If you built custom chat features or a frontend against these URLs, plan to migrate them together to the current Discuss data flow.
- Website & Customer Portal (quick pass for most): Portal-side helpers were trimmed — the routine that added an attachment from the portal, the one that validated the address/details form, and the content builder behind the 'reorder' modal on a customer's past orders. If your module customises the portal address form or the reorder feature, re-check those; otherwise this is low risk.
- Point of Sale / IoT (close look only if you ship hardware code): The IoT display controller lost its display, refresh, and device-listing routes, and the driver controller's certificate check was removed. This matters only to custom IoT box drivers or display integrations.
- Web client plumbing (quick pass): The old DataSet resequence endpoint used by legacy list reordering was removed; if you still call it from custom JS, move to the standard resequencing.
Beyond these, 25,749 smaller internal changes exist — compute helpers and body-only edits — and are only relevant if you override Odoo's private helpers.
Your migration checklist
- Install your module against a clean Odoo 20 database and resolve any import or manifest errors before touching logic.
- Grep your Python and XML for the removed binary-field hooks and any custom binary/attachment field subclasses, and confirm nothing relies on the old bin_size or column-type behaviour.
- Search your JavaScript and controllers for direct calls to the removed Discuss, calendar-credential, IoT display, and portal routes, and repoint or remove each one.
- Rework the accounting ZIP-download feature if your module links to or overrides that endpoint.
- Check any low-level query code for use of the removed cursor ID-splitting helper and replace it with your own batching.
- Run your full test suite on Odoo 20 and exercise every affected workflow end to end before go-live.