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 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.
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.0 | Odoo 20.0 | |
|---|---|---|
| Addons (community + enterprise) | 1476 | 1574 |
| Addon directories removed / added | — | 150 removed, 248 added |
| Distinct model names (approx.) | ~1692 | ~2020 |
ai* addons | 15 | 51 |
l10n_* addons | 552 | 558 |
| Minimum Python | 3.10 | 3.12 |
| Minimum PostgreSQL | 13 | 16 |
| OWL (frontend framework) | 2.8.4 | 3.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:
pytzis no longer in the requirements. Onlytzdataremains, and only on Windows. Odoo appears to have moved to the standard library'szoneinfo. A custom module that importspytzkeeps working only if you install the package yourself, so add it to your own requirements.h11is a new dependency.odoo/http.pyis now a package (odoo/http/withrouter.py,routing_map.py,server.py,session.py,stream.pyand 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 newbase_report_paper_muncheradds Odoo's own HTML/CSS-to-PDF engine, Paper Muncher. The engine is chosen inir.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_paramandset_paramare replaced byget_str,get_int,get_bool,get_floatand the matchingset_*methods. - Binary values are objects. Inside Python, a binary field reads as a
BinaryValue/BinaryByteswith.content,.decode()and.to_base64(). Writing rawbytesraisesTypeError, except onir.attachment.raw. ir.attachment.datasis gone. Useraw;db_datasis the storage column.- Domains are first-class
Domainobjects, andfrozendictgives way toMappingProxyType. odoo/osvis gone. New modulesodoo/orm/cache.py,query.pyandmodels_cached.pyappear.- Cache invalidation moves from
registry.clear_cachetotransaction.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.
- 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)sonly, andscopeis 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. - Binary fields read as a dict.
readandsearch_readnow return{"content": "<base64>", "filename": "...", "size": 48213}where 19.0 returned a bare base64 string. - Models and fields were renamed or removed.
stock.move.product_uomis nowuom_id,hr.leave.typemerged intohr.work.entry.type,account.groupandres.bankare gone, andstock.scrapis no longer a model. - XML-RPC and JSON-RPC are on a clock.
/xmlrpc,/xmlrpc/2and/jsonrpcstill 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. - Odoo 20 ships an MCP server. The new Enterprise addon
ai_mcpexposesPOST /mcpfor 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.batchlives instock). Scrap is no longer a document; a scrap is astock.moveflaggedis_scrapwith reason tags. UoM fields are unified onuom_idacross moves, move lines, purchase lines and BoMs. An outgoing move'sstock.move.valueis stored negative again. - Manufacturing.
mrp.bom.consumptionis gone. The consumption check now runs on every manufacturing order and shows a confirmable warning; there is no "flexible" mode that skips it. A newmrp.bom.extra_costmodel carries per-unit labour, energy or packaging cost. - Point of Sale.
point_of_saleno longer depends on Inventory; stock handling moved to the auto-installedpos_stock. Session closing was rewritten: it now posts oneout_invoicefor sales and oneout_refundfor refunds, and the Force Close wizard is gone. Kitchen display models moved from Enterprise into communitypoint_of_sale. - Accounting.
account.groupis replaced by a hierarchy onaccount.account.parent_id, andres.bankis removed; bank name and address live onres.partner.bank. - HR.
hr.leave.typemerged intohr.work.entry.type(labelled "Time Type"), andhr.contract.typebecamehr.employee.type. - Field Service was rebuilt on Planning: the eight
industry_fsm*addons are replaced by twelveplanning_field_service*addons. - AI. The
ai*family grew from 15 to 51 addons, includingai_agentic(agents triggered from automation rules) andai_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_batch | stock, stock_barcode |
website_sale_wishlist, website_sale_comparison*, website_sale_autocomplete | website_sale, website_address_autocomplete |
base_vat, base_iban | base |
industry_fsm* (8 addons) | planning_field_service* |
iot_base, iot_box_image | obox*, iot_webserial |
purchase_requisition_sale | purchase_alternative_sale |
hr_homeworking* | hr (hr.employee.location) |
esg_csrd* | esg |
account_peppol_response, account_peppol_advanced_fields | account_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
- Rebuild the runtime first: Python 3.12+, PostgreSQL 16+, and add
pytzto your own requirements if your code imports it. - Run Odoo's source-upgrade scripts on your custom modules, then convert security files to
ir.access.csv. Map any group-less ACL row tobase.group_everyone, as Odoo's own converter does. - Replace Font Awesome icons with Material Symbols names, and click every custom button.
- 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. - Regenerate integration API keys with the RPC scope and test every external client against a 20.0 copy before cut-over.
- Plan the move off XML-RPC/JSON-RPC before Odoo 22 removes them.
The rest of the series
- Part 2: Odoo 20 API changes that break integrations silently
- Part 3: Odoo XML-RPC is deprecated: migrating to the JSON-2 API
- Part 4: Odoo 20's built-in MCP server: what it exposes
- Part 5: Migrating custom modules to Odoo 20: the silent breaks
For the API fundamentals that did not change between versions, start with how to authenticate to the Odoo API and the ODXProxy API reference.