← Blog
odooodoo-20upgrademigrationdevelopers

Odoo 19 vs Odoo 20: What Changed for Developers

Odoo 19 vs Odoo 20 from the source code: Python 3.12, PostgreSQL 16, ir.access, scoped API keys, OWL 3, and which removed modules were really just merged.

ODXProxy Team · Oct 1, 2026 · 9 min read

Odoo 19 vs Odoo 20: What Changed for Developers — ODXProxy blog cover

Odoo 20.0 shipped on 2026-09-24, and the question every developer and integrator is now asking is the same: Odoo 19 vs Odoo 20, what actually changed for my code? Odoo's release notes describe features. This guide describes what changed in the source. We compared two Enterprise source trees, odoo-19.0+e.20260902 and odoo-20.0+e.20260924, and ported five real custom modules from 19.0 to 20.0. What follows is the developer view: platform requirements, framework changes, the external API, and the module reshuffle. File references point into the 20.0 tree, so you can check any claim yourself.

This is part 1 of a five-part series on moving from Odoo 19 to Odoo 20. The deep dives cover API changes that break integrations, the XML-RPC deprecation and the JSON-2 API, Odoo 20's built-in MCP server, and porting custom modules.

The release in numbers

These counts are exact for the two snapshots above. They come from listing directories and grepping _name declarations, so treat the model count as approximate.

Odoo 19.0Odoo 20.0
Addons (community + enterprise)14761574
Addon directories removed / added—150 removed, 248 added
Distinct model names (approx.)~1692~2020
ai* addons1551
l10n_* addons552558
Minimum Python3.103.12
Minimum PostgreSQL1316
OWL (frontend framework)2.8.43.0.0-alpha.49

The "150 removed" figure overstates the churn. Many of those addons did not disappear; they were folded into a parent module. Batch transfers moved into stock, wishlist and product comparison moved into website_sale, and base_vat and base_iban moved into base. There is a full table further down.

Platform: Python 3.12 and PostgreSQL 16

The minimums are set in odoo/release.py: MIN_PY_VERSION = (3, 12) and MIN_PG_VERSION = 16. That rules out the system Python on Ubuntu 22.04 and Debian 11. The 20.0 requirements.txt drops every python_version < '3.12' pin and targets Ubuntu 24.04 and Debian 13.

Three dependency changes matter for custom code:

  • pytz is no longer in the requirements. Only tzdata remains, and only on Windows. Odoo appears to have moved to the standard library's zoneinfo. A custom module that imports pytz keeps working only if you install the package yourself, so add it to your own requirements.
  • h11 is a new dependency. odoo/http.py is now a package (odoo/http/ with router.py, routing_map.py, server.py, session.py, stream.py and more). The current request is a context variable (request_var) rather than a stack.
  • PDF rendering is pluggable. base_report_wkhtmltox (auto-installed) wraps the classic wkhtmltopdf engine, and the new base_report_paper_muncher adds Odoo's own HTML/CSS-to-PDF engine, Paper Muncher. The engine is chosen in ir.actions.report._get_pdf_engine(). wkhtmltopdf has been unmaintained upstream for years, and this is the start of Odoo's way off it.

On the operations side, the db RPC service is gone. In 19.0 the RPC dispatcher routed common, db and object. In 20.0 dispatch_rpc (odoo/http/router.py) accepts only common and object and raises ValueError for anything else. Database list, create, backup, restore and drop now exist only as web routes under /web/database/*. Any backup script that called db.dump over XML-RPC needs rewriting.

Framework: the changes custom modules feel first

One access model: ir.access

ir.model.access and ir.rule no longer exist. A single model, ir.access, replaces both. Module security files become security/ir.access.csv with the header id,name,model_id,group_id/id,operation,domain. model_id is now the model name (purchase.order) rather than a model_* xmlid, and operation is a subset of crud:

id,name,model_id,group_id/id,operation,domain
access_account_asset,account.asset,account.asset,account.group_account_readonly,r,
access_account_asset_manager,account.asset,account.asset,account.group_account_user,crud,

The semantics are OR of permissions, AND of restrictions. A row with a group grants access, and grants add up as they did before. A row without a group restricts access, the way a global record rule did. The trap: a model whose only rows are group-less grants nobody anything. Part 5 of this series covers that in detail.

ORM and Python API

  • Typed config parameters. ir.config_parameter.get_param and set_param are replaced by get_str, get_int, get_bool, get_float and the matching set_* methods.
  • Binary values are objects. Inside Python, a binary field reads as a BinaryValue / BinaryBytes with .content, .decode() and .to_base64(). Writing raw bytes raises TypeError, except on ir.attachment.raw.
  • ir.attachment.datas is gone. Use raw; db_datas is the storage column.
  • Domains are first-class Domain objects, and frozendict gives way to MappingProxyType.
  • odoo/osv is gone. New modules odoo/orm/cache.py, query.py and models_cached.py appear.
  • Cache invalidation moves from registry.clear_cache to transaction.invalidate_ormcache.

Odoo ships its own source-upgrade scripts for this hop, odoo/upgrade_code/19.1-… through 19.5-…, plus owl3-migration.py. They handle the mechanical renames. They do not catch everything, as part 5 shows.

Web client: OWL 3 and no Font Awesome

The backend runs OWL 3 (shipped as 3.0.0-alpha.49), so any custom JavaScript module needs a pass, ideally through Odoo's owl3-migration.py transform first.

The quieter change is the icon set. The backend bundle now includes only Material Symbols (a subset font) and odoo_ui_icons. A button with icon="fa-copy", an <i class="fa fa-…"> element, or a fa- activity icon renders blank, with no error. Use a Material Symbols name that core already uses, such as content_copy, check_circle or verified, because the font is subset and an unused name may not be in it.

The external API: what integrators need to know

This is the section to read if you maintain anything that talks to Odoo from outside. Each item is covered in depth in part 2, Odoo 20 API changes that break integrations.

  1. API keys are strictly scoped. In 19.0 a key with no scope was a global key. In 20.0 the credential check matches scope = %(scope)s only, and scope is required. That check also runs when a key is used as the password on XML-RPC or JSON-RPC, so a key that worked against 19.0 may simply stop authenticating.
  2. Binary fields read as a dict. read and search_read now return {"content": "<base64>", "filename": "...", "size": 48213} where 19.0 returned a bare base64 string.
  3. Models and fields were renamed or removed. stock.move.product_uom is now uom_id, hr.leave.type merged into hr.work.entry.type, account.group and res.bank are gone, and stock.scrap is no longer a model.
  4. XML-RPC and JSON-RPC are on a clock. /xmlrpc, /xmlrpc/2 and /jsonrpc still work in 20.0, but every request logs that they are "deprecated in Odoo 19 and scheduled for removal in Odoo 22". Part 3 covers migrating to the JSON-2 API.
  5. Odoo 20 ships an MCP server. The new Enterprise addon ai_mcp exposes POST /mcp for AI clients. Part 4 covers what it exposes.

If you run several Odoo databases, expect a period where some are on 19.0 and some on 20.0. That is when a single integration contract in front of all of them pays off. With ODXProxy, every request names its target instance, and POST /api/odoo/version returns each instance's server_version_info (for 20.0 it starts [20, 0, 0, ...]), so an app can check which shapes to expect before it reads a record.

Functional changes developers should know about

You don't need every functional change to port code, but these ones change data shapes or behaviour that custom code depends on.

  • Inventory. Batch transfers are core (stock.picking.batch lives in stock). Scrap is no longer a document; a scrap is a stock.move flagged is_scrap with reason tags. UoM fields are unified on uom_id across moves, move lines, purchase lines and BoMs. An outgoing move's stock.move.value is stored negative again.
  • Manufacturing. mrp.bom.consumption is gone. The consumption check now runs on every manufacturing order and shows a confirmable warning; there is no "flexible" mode that skips it. A new mrp.bom.extra_cost model carries per-unit labour, energy or packaging cost.
  • Point of Sale. point_of_sale no longer depends on Inventory; stock handling moved to the auto-installed pos_stock. Session closing was rewritten: it now posts one out_invoice for sales and one out_refund for refunds, and the Force Close wizard is gone. Kitchen display models moved from Enterprise into community point_of_sale.
  • Accounting. account.group is replaced by a hierarchy on account.account.parent_id, and res.bank is removed; bank name and address live on res.partner.bank.
  • HR. hr.leave.type merged into hr.work.entry.type (labelled "Time Type"), and hr.contract.type became hr.employee.type.
  • Field Service was rebuilt on Planning: the eight industry_fsm* addons are replaced by twelve planning_field_service* addons.
  • AI. The ai* family grew from 15 to 51 addons, including ai_agentic (agents triggered from automation rules) and ai_mcp.

Merged, not removed: where the features went

When an upgrade report says an addon was removed, check this table before you plan a rewrite.

Removed 19.0 addon(s)Where the feature lives in 20.0
stock_picking_batch, delivery_stock_picking_batch, stock_barcode_picking_batchstock, stock_barcode
website_sale_wishlist, website_sale_comparison*, website_sale_autocompletewebsite_sale, website_address_autocomplete
base_vat, base_ibanbase
industry_fsm* (8 addons)planning_field_service*
iot_base, iot_box_imageobox*, iot_webserial
purchase_requisition_salepurchase_alternative_sale
hr_homeworking*hr (hr.employee.location)
esg_csrd*esg
account_peppol_response, account_peppol_advanced_fieldsaccount_peppol
pos_blackbox_be, l10n_be_pos_blackbox*l10n_be_pos

A custom module that declared one of these in its manifest depends needs the new parent instead, and any xmlid it referenced now carries the new module prefix.

An upgrade checklist for developers

  1. Rebuild the runtime first: Python 3.12+, PostgreSQL 16+, and add pytz to your own requirements if your code imports it.
  2. Run Odoo's source-upgrade scripts on your custom modules, then convert security files to ir.access.csv. Map any group-less ACL row to base.group_everyone, as Odoo's own converter does.
  3. Replace Font Awesome icons with Material Symbols names, and click every custom button.
  4. Grep for removed names (get_param, datas, product_uom, stock.scrap, hr.leave.type), then look for changed attributes such as sign flips and dropped size limits, which a grep will not find.
  5. Regenerate integration API keys with the RPC scope and test every external client against a 20.0 copy before cut-over.
  6. Plan the move off XML-RPC/JSON-RPC before Odoo 22 removes them.

The rest of the series

For the API fundamentals that did not change between versions, start with how to authenticate to the Odoo API and the ODXProxy API reference.