What's New in Odoo 20: Breaking Changes for Community Upgrades
Last updated: October 2026
Odoo 20.0 was released in late September 2026. For anyone who runs Odoo Community Edition on their own server, three things matter first: the server requirements went up, the access rights system was rewritten, and a long list of models that held business data in 19 (banks, time off types, account groups, overtime) are gone or restructured.
This page lists the changes we checked line by line in the 19.0 and 20.0 Community source on GitHub, with the file where each one lives. It is written for developers, integrators and admins planning an upgrade. Enterprise-only modules are not covered.
The short version
- Odoo 20 needs Python 3.12 or newer and PostgreSQL 16 or newer. Odoo 19 needed Python 3.10 and PostgreSQL 13.
ir.model.accessandir.ruleare replaced by a single model,ir.access. Modules shipsecurity/ir.access.csv.res.bankis merged intores.partner.bank;hr.leave.typeintohr.work.entry.type;account.groupinto a parent/child hierarchy onaccount.account.hr.work.entryis no longer a model in Community, and overtime is now recorded as attendance records produced byhr.time.rule.- The web client moves from OWL 2 to OWL 3, with a compatibility layer and an automatic rewrite script for custom code.
- OCU migrates Community databases to 20, from 19 or in one upload from 15 to 18. Plan the PostgreSQL 16 server first.
Release facts and server requirements
The 20.0 branch of odoo/odoo declares version_info = (20, 0, 0, FINAL, 0, '') in odoo/release.py. The same file raises the minimum versions:
| Requirement | Odoo 19 | Odoo 20 |
|---|---|---|
| Python (MIN_PY_VERSION) | 3.10 | 3.12 |
| PostgreSQL (MIN_PG_VERSION) | 13 | 16 |
The two checks behave differently. The Python check is an assertion in odoo/init.py, so Odoo 20 does not start on Python 3.11 or older. The PostgreSQL check in odoo/sql_db.py only emits a warning when the server is older than 16. Odoo will connect, but you are running an unsupported combination.
What that means for self-hosted servers:
- Ubuntu 24.04 ships Python 3.12 and PostgreSQL 16, which matches Odoo 20 out of the box.
- Ubuntu 22.04 (Python 3.10, PostgreSQL 14) and Debian 12 (Python 3.11, PostgreSQL 15) do not. You need a newer OS, a separately installed Python and PostgreSQL, or containers.
- A PostgreSQL major-version upgrade is its own project:
pg_upgradeor a dump and restore into the new cluster. Do it, and test it, before the Odoo upgrade so the two changes do not land on the same night.
How big is the 19 to 20 step?
186
Models added or removed
194
Constraint changes
50
Modules gone
50
New modules
Counted from the Odoo source of each version and from the OCU migration mappings, step by step; Odoo’s internal test modules are left out because they never exist in a production database. Multi-step paths add up their steps. Counted on the 19.0 and 20.0 branches as of October 15, 2026.
Of the 186 models, 123 are new and 63 no longer exist. For comparison, the 18 to 19 step adds or removes 129 models and has 207 constraint changes. Most of the 50 modules that disappear were merged into another module rather than dropped; the merges are listed below with each area.
Access rights: ir.model.access and ir.rule become ir.access
This is the change with the widest reach. In Odoo 19, model-level access lived in ir.model.access (one row per group with four booleans) and record-level filtering in ir.rule (domains, global or per group). In Odoo 20 both models are removed. A single model, ir.access, defined in odoo/addons/base/models/ir_access.py, does both jobs:
group_id: one optional group (Many2one, not Many2many asir.rule.groupswas).operation: a selection that is a subset ofcrud, for exampler,ruorcrud. It replaces theperm_read/perm_write/perm_create/perm_unlinkbooleans.domain: optional. Without a domain the record applies to all records of the model.model_id,name,active, plus a computedkindthat ispermissionwhen a group is set andrestrictionwhen it is not.
The way records combine also changed. The ORM collects every applicable access for the user and returns Domain.OR(permissions) & Domain.AND(restrictions) (odoo/orm/models.py). A group record therefore both grants the operation and, when it has a domain, limits it to those records. A groupless record narrows what every user can do, which is how global record rules behaved.
Modules now ship security/ir.access.csv (base itself does) with the columns id,name,model_id,group_id/id,operation,domain:
odoo/addons/base/security/ir.access.csv (20.0)
id,name,model_id,group_id/id,operation,domain
access_decimal_precision_config,decimal.precision configuration,decimal.precision,base.group_system,ru,
ir_attachment_public_rule,read public attachments,ir.attachment,base.group_user,r,"[('public', '=', True)]"Two consequences for an upgrade:
- Source code: the
upgrade_codescript19.4-00-ir-access.pyconverts a module’sir.model.access.csvlines andir.ruleXML records intoir.access.csv. Its own log says WARNING lines mark partially handled cases that need a manual check. - Database: we found no code in the 20.0 Community source that converts the existing
ir_model_accessandir_rulerows of a 19 database. That conversion is the upgrade tool’s job, and it includes rules that admins created in Settings › Technical and that exist in no module.
After an upgrade, check access as a portal user and as a restricted internal user, not only as admin. An access rule that converts too broadly shows no error; users simply see more than they should.
Framework and web client
- OWL 3: the bundled
web/static/lib/owl/owl.jsreports version3.0.0-alpha.49in 20.0, against2.8.4in 19.0. A compatibility layer (web/static/src/owl2/owl3_compatibility_layer.js) lets OWL 2 code run with small changes:t-portalbecomest-custom-portal,t-modelbecomest-custom-model, anduseEffectbecomesuseLayoutEffect. - Search date filters: the
start_month,end_month,start_yearandend_yearattributes ondatefilters are, in the words of the20.0-00-search-date-filtersscript, "not parsed anymore, nor accepted by the RNG". A custom search view that still uses them fails validation. - ORM cache:
Registry.clear_cache()no longer exists. The replacement isinvalidate_ormcache()on the transaction (odoo/orm/environments.py). - Reports: wkhtmltopdf support moves into its own module,
base_report_wkhtmltox, which is auto-installed. A second engine,base_report_paper_muncher, uses Paper Muncher, Odoo’s own rendering engine; it is optional.
Accounting
account.groupis removed.account.accountis now a parent store itself, withparent_id,parent_pathand a computedparent_ids(addons/account/models/account_account.py). In 19, groups were matched to accounts by code prefix throughgroup_id. The19.3-00-account-groupsupgrade_code script converts the group data of 23 charts of accounts (among themus,mx,coandar_ri) into parent accounts.account.move.line.deductible_amount(a percentage, default 100) is replaced bydeductible_percentage(a fraction, default 1.0). Converting the data means dividing by 100, not copying the column.- Payment states moved one step along, so
paidchanges meaning. In 19 a payment wentin_process→paid; in 20 it goespaid→reconciled(addons/account/models/account_payment.py,_compute_state). A payment that was In Process in 19 is Paid in 20, and one that was Paid in 19 is Reconciled. Saved filters, reports, server actions and custom code that teststate = 'paid'will match different payments after the upgrade. - Bank accounts: see the
res.bankmerge under Contacts below;accountadds IBAN validation tores.partner.bankitself, andbase_ibanis gone. - VIES VAT checking lives in a new module,
l10n_eu_account_vies, which is not auto-installed.base_vatno longer exists. account_add_glnis merged intoaccount, andaccount_peppol_responseintoaccount_peppol.
HR, time off and attendance
HR changed in 19 (contracts became hr.version) and changes again in 20. If you use Time Off, Attendances or Work Entries, expect the most data conversion here.
Time off types are work entry types
hr.leave.type no longer exists. hr_holidays now depends on hr_work_entry and adds its time off settings (allocation rules, dashboard visibility, calendar display) to hr.work.entry.type (addons/hr_holidays/models/hr_work_entry_type.py). The links follow:
| Model | Odoo 19 field | Odoo 20 field |
|---|---|---|
| hr.leave | holiday_status_id | work_entry_type_id (hr_leave.py) |
| hr.leave.allocation | holiday_status_id | work_entry_type_id (hr_leave_allocation.py) |
| hr.leave.accrual.plan | time_off_type_id | work_entry_type_id (hr_leave_accrual_plan.py) |
hr_work_entry_holidays is merged into hr_holidays.
Work entries are no longer stored in Community
The hr.work.entry model is not part of the 20.0 Community source. hr_work_entry keeps work entry types, time rules and a new export wizard (hr.export.work.entries). hr.version.generate_work_entries() now returns a list of value dictionaries instead of creating hr.work.entry records, and its force parameter is gone (addons/hr_work_entry/models/hr_version.py). Custom code that searched or created work entries in Community needs a rethink, and work entry rows in a 19 database have no Community model to land in.
Overtime becomes time rules
hr.attendance.overtime.line, hr.attendance.overtime.rule and hr.attendance.overtime.ruleset are removed. Their role goes to hr.time.rule in hr_work_entry (thresholds, employee domain, country, company) and to hr.attendance itself, which gains time_rule_id, source_attendance_id and overtime_attendance_ids. Overtime is recorded as attendance records generated from a source attendance.
Employee types and departures
hr.contract.typeis replaced byhr.employee.type:contract_type_idbecomesemployee_type_idonhr.version,hr.joband (related)hr.employee.hr.departure.wizardis replaced by a stored model,hr.employee.departure(addons/hr/models/hr_employee_departure.py). Onhr.version,departure_reason_id,departure_descriptionanddeparture_datebecome related fields of the newdeparture_id, so the stored values move to the departure record. Adismissal_dateis new.hr_org_chart,hr_hourly_costandhr_homeworkingare merged intohr(hr.employee.locationnow lives inhr).
Contacts and bank accounts
res.bank is merged into res.partner.bank
Odoo 20 has no res.bank model and no bank_id on bank accounts. The bank’s name, BIC and address are stored on each res.partner.bank row (odoo/addons/base/models/res_partner_bank.py): bank_name and bank_bic become stored fields instead of related ones, and street, street2, zip, city, state_id and country_id are new. Several fields are renamed:
| Odoo 19 field | Odoo 20 field | Note |
|---|---|---|
| acc_number | account_number | Also the _rec_name |
| sanitized_acc_number | sanitized_account_number | Stored, computed |
| acc_holder_name | holder_name | Stored, computed, editable |
| acc_type | account_type | Computed selection |
| bank_id.name / bank_id.bic | bank_name / bank_bic | Copied from the bank record |
Any report, export, server action or integration that reads acc_number or bank_id needs updating. Payment file exports (SEPA and similar) are the usual place this shows up.
Partner identifiers
company_registryno longer exists onres.partnerorres.companyin the 20.0 Community source.res.partnergetsadditional_identifiers, a JSON field holding further identifiers by key. Localizations declare the identifiers they support by overriding_get_all_additional_identifiers_metadata(Argentina, Brazil, Chile, Colombia, Dominican Republic, Ecuador, Guatemala, Panama, Peru, Saudi Arabia, Spain and Uruguay do in 20.0).l10n_latam_baseis removed together with itsl10n_latam.identification.typemodel.
Inventory, purchase and sales
Unit-of-measure fields are renamed to uom_id on several models:
| Model | Odoo 19 field | Odoo 20 field |
|---|---|---|
| stock.move | product_uom | uom_id |
| stock.move.line | product_uom_id | uom_id |
| stock.warehouse.orderpoint | product_uom | uom_id |
| repair.order | product_uom | uom_id |
| purchase.order.line | product_uom_id | uom_id |
sale.order.line and account.move.line keep product_uom_id. Other changes:
stock.move.location_final_idis renamedforecasted_location_id.stock.scrapis removed. A scrap is now astock.movewithis_scrapset and optionalscrap_reason_tag_ids; the 19 fieldstock.move.scrap_idis gone.- The return wizard (
stock.return.pickingand its lines) is removed.stock.picking.action_return()creates the return picking directly.sale_stockadds areturn.reasonmodel. stock_picking_batchis merged intostock:stock.picking.batchis now inaddons/stock.product.template.attribute.exclusionis removed. Exclusions are a Many2many onproduct.template.attribute.value,excluded_value_ids, limited to values of the same product template.point_of_saleno longer depends onstock_account. Stock handling for POS moves to a new, auto-installedpos_stockmodule.- RFQ alternatives move out of
purchase_requisitioninto a newpurchase_alternativemodule (purchase.alternative.createreplacespurchase.requisition.create.alternative). - Pricelist rules: the
compute_pricevaluespercentageandformulaare gone. Odoo 20 hasdiscount,markup(shown as Surcharge) andfixed(addons/product/models/product_pricelist_item.py).price_markupis stored as the negative ofprice_discount. Every existing percentage or formula rule has to be mapped to one of the new values.
Discuss, website and other apps
- Chatter history:
mail.tracking.valueandmail.message.tracking_value_idsmove out ofmailinto a new module,mail_tracking, which is not auto-installed. Without it,mailwrites tracked field changes into the message body. A 19 database stores its field-change history asmail_tracking_valuerows, so check that old chatter entries still show their changes after an upgrade. website_sale_wishlistandwebsite_sale_comparisonare merged intowebsite_sale(product.wishlistandproduct.attribute.categorynow live there).- The base unit used for unit prices moves from eCommerce to products:
website.base.unitbecomesproduct.base.unit, andbase_unit_idis defined inproductinstead ofwebsite_sale. website_sale_autocompleteis gone; address autocompletion is in the newwebsite_address_autocomplete, which no longer needs eCommerce.- Fleet:
fleet.vehicle.log.services.dateis replaced bydate_from, with a new optionaldate_to.
New in Odoo 20 Community
Features we could confirm from new models and module manifests in the 20.0 Community source:
- Polls in Discuss (
mail.poll,mail.poll.option,mail.poll.voteinmail). - An llms.txt editor for websites (
website.llms, stored inwebsite.llms_txt). - Withdrawal requests for online orders (
withdrawal.requestinwebsite_sale, linked to a sale order). - Return reasons on returns (
return.reasoninsale_stock). - POS preparation orders and courses (
pos.prep.order,pos.courseinpoint_of_sale). - Personal calendars (
calendar.calendar, "User Calendar", incalendar). - Paper Muncher as an optional PDF engine (
base_report_paper_muncher). - New bridge modules, including
fleet_maintenance,crm_sale_project(projects from opportunities),website_sale_project,sale_project_margin,mrp_deliveryandpos_bancontact_pay(Bancontact Pay and Wero in POS). - New localizations for North Macedonia (
l10n_mk) and Myanmar (l10n_mm), and Pakistan e-invoicing (l10n_pk_edi).
What this means for custom modules
Odoo ships rewrite scripts for module source code in odoo/upgrade_code/. They show what Odoo itself treats as source-level breaking changes. These are new between 19.0 and 20.0:
| Script | Rewrites | Notes |
|---|---|---|
| 19.1-00-t-call.py | QWeb XML | t-set inside a t-call body becomes a parameter of the t-call |
| 19.3-00-account-groups.py | Chart templates | Account groups to parent accounts |
| 19.3-00-account-report-foldable.py | Account report XML | Drops foldable on report lines where it matches the computed value |
| 19.3-00-base64-in-xml.py | XML data | type="base64" file fields to type="bytes" |
| 19.4-00-ir-access.py | Security CSV/XML | Access CSV and record rules to ir.access.csv |
| 19.4-00-ormcache-on-transaction.py | Python | registry.clear_cache to transaction.invalidate_ormcache |
| 19.5-00-tuple-rec_names_search.py | Python | _rec_names_search lists to tuples |
| 20.0-00-search-date-filters.py | Views | Drops removed date filter attributes |
| owl3-migration.py | JS and OWL templates | OWL 2 to OWL 3 (hooks, t-esc, t-ref, t-model, t-portal, ...) |
Run them against a copy of your addons, first with --dry-run. The versioned scripts run with --from; the OWL script has no version in its name, so --from skips it and you call it by name:
./odoo-bin upgrade_code --addons-path=/path/to/custom_addons --from 19.1 --dry-run
./odoo-bin upgrade_code --addons-path=/path/to/custom_addons --from 19.1
./odoo-bin upgrade_code --addons-path=/path/to/custom_addons --script owl3-migrationThe command’s own documentation calls the scripts best-effort: “they only help do the heavy-lifting, they are not silver bullets.” Model removals (res.bank, hr.leave.type, account.group, stock.scrap, hr.work.entry) and field renames are not handled by any script; search your code for them by hand.
Upgrade options for Community today
As of October 16, 2026:
- OpenUpgrade: the 20.0 branch of OCA/OpenUpgrade contains only its initial commit from September 28, 2026, with no migration scripts yet. OpenUpgrade coverage for a new version usually builds up over many months.
- Odoo’s upgrade service is part of an Enterprise subscription. According to Cybrosys (Oct 5, 2026), neither it nor Odoo.sh offered 20.0 as of September 30, 2026.
- OCU: migrates Community databases from 19 to 20, and from 15, 16, 17 or 18 to 20 in one upload by chaining the steps. Custom access rights and global record rules are converted to
ir.access; record rules tied to a group are kept and listed for review. See the Odoo 19 to 20 guide.
Whether to go straight to 20 or stop at 19 mostly depends on your third-party modules and your server. If a module you rely on has no 20.0 version yet, or the server cannot run PostgreSQL 16 yet, upgrade to 19 now and take the last step later. Otherwise one upload to 20 saves a second migration. A free test migration to 20 shows how your database comes through either way. If you are on 18, see the Odoo 18 to 20 guide; for older versions, the upgrade guide covers every path.
Frequently asked questions
- When was Odoo 20 released?
- Odoo 20.0 was released in late September 2026, at Odoo Experience in Brussels. The 20.0 branch of the Community source on GitHub is marked as a final release (version_info = (20, 0, 0, FINAL, 0) in odoo/release.py).
- Does Odoo 20 require PostgreSQL 16?
- Yes. odoo/release.py sets MIN_PG_VERSION = 16 and MIN_PY_VERSION = (3, 12). Odoo 19 required PostgreSQL 13 and Python 3.10. Odoo 20 refuses to start on an older Python; on an older PostgreSQL it logs a warning, but that setup is unsupported. Ubuntu 24.04 ships both PostgreSQL 16 and Python 3.12.
- Can I upgrade Odoo 19 Community to Odoo 20 today?
- Yes. Since October 16, 2026, OCU migrates Odoo 19 Community databases to 20, and takes 15, 16, 17 and 18 databases to 20 in one upload by chaining the steps. The OpenUpgrade 20.0 branch holds only its initial commit, and Odoo’s own upgrade service is for Enterprise customers. Before the production run, plan the PostgreSQL 16 upgrade and run upgrade_code on your custom modules.
- What happens to ir.model.access and ir.rule in Odoo 20?
- Both models are removed and replaced by ir.access. Each ir.access record has one optional group, an operation (a subset of create, read, update, delete) and an optional domain. Records with a group grant access and are combined with OR; records without a group restrict access and are combined with AND. Modules ship security/ir.access.csv instead of ir.model.access.csv. Existing rows in a database have to be converted during the upgrade.
- Will my custom Odoo 19 modules work on Odoo 20?
- Most will need changes. Access CSV files and record rules must become ir.access records, models such as res.bank, hr.leave.type and account.group no longer exist, several fields are renamed, and the web client moves to OWL 3. Odoo’s odoo-bin upgrade_code command rewrites part of this automatically; review the result and test.
- Should I upgrade to Odoo 19 or go straight to 20?
- With OCU, one upload takes a 15, 16, 17 or 18 database to either version. Stop at 19 if a third-party module you rely on has no 20.0 version yet, or if your server cannot run PostgreSQL 16 yet; 19 has had a year of fixes. Otherwise going to 20 saves a second migration later.
Further reading
- Migrate Odoo 19 to 20: what OCU converts, server requirements and a pre-flight checklist.
- Migrate Odoo 18 to 20: both steps in one upload.
- What's New in Odoo 19: the previous release's breaking changes.
- How to Upgrade Odoo Community Edition: options, preparation and every supported path.
- Odoo Migration Checklist: what to check before, during and after an upgrade.
Upgrade to Odoo 20
Free test migrations from 15, 16, 17, 18 or 19. No credit card required.
Start your free test migration