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.access and ir.rule are replaced by a single model, ir.access. Modules ship security/ir.access.csv.
  • res.bank is merged into res.partner.bank; hr.leave.type into hr.work.entry.type; account.group into a parent/child hierarchy on account.account.
  • hr.work.entry is no longer a model in Community, and overtime is now recorded as attendance records produced by hr.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:

RequirementOdoo 19Odoo 20
Python (MIN_PY_VERSION)3.103.12
PostgreSQL (MIN_PG_VERSION)1316

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_upgrade or 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 as ir.rule.groups was).
  • operation: a selection that is a subset of crud, for example r, ru or crud. It replaces the perm_read/perm_write/perm_create/perm_unlink booleans.
  • domain: optional. Without a domain the record applies to all records of the model.
  • model_id, name, active, plus a computed kind that is permission when a group is set and restriction when 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_code script 19.4-00-ir-access.py converts a module’s ir.model.access.csv lines and ir.rule XML records into ir.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_access and ir_rule rows 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.js reports version 3.0.0-alpha.49 in 20.0, against 2.8.4 in 19.0. A compatibility layer (web/static/src/owl2/owl3_compatibility_layer.js) lets OWL 2 code run with small changes: t-portal becomes t-custom-portal, t-model becomes t-custom-model, and useEffect becomes useLayoutEffect.
  • Search date filters: the start_month, end_month, start_year and end_year attributes on date filters are, in the words of the 20.0-00-search-date-filters script, "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 is invalidate_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.group is removed. account.account is now a parent store itself, with parent_id, parent_path and a computed parent_ids (addons/account/models/account_account.py). In 19, groups were matched to accounts by code prefix through group_id. The 19.3-00-account-groups upgrade_code script converts the group data of 23 charts of accounts (among them us, mx, co and ar_ri) into parent accounts.
  • account.move.line.deductible_amount (a percentage, default 100) is replaced by deductible_percentage (a fraction, default 1.0). Converting the data means dividing by 100, not copying the column.
  • Payment states moved one step along, so paid changes meaning. In 19 a payment went in_process → paid; in 20 it goes paid → 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 test state = 'paid' will match different payments after the upgrade.
  • Bank accounts: see the res.bank merge under Contacts below; account adds IBAN validation to res.partner.bank itself, and base_iban is gone.
  • VIES VAT checking lives in a new module, l10n_eu_account_vies, which is not auto-installed. base_vat no longer exists.
  • account_add_gln is merged into account, and account_peppol_response into account_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:

ModelOdoo 19 fieldOdoo 20 field
hr.leaveholiday_status_idwork_entry_type_id (hr_leave.py)
hr.leave.allocationholiday_status_idwork_entry_type_id (hr_leave_allocation.py)
hr.leave.accrual.plantime_off_type_idwork_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.type is replaced by hr.employee.type: contract_type_id becomes employee_type_id on hr.version, hr.job and (related) hr.employee.
  • hr.departure.wizard is replaced by a stored model, hr.employee.departure (addons/hr/models/hr_employee_departure.py). On hr.version, departure_reason_id, departure_description and departure_date become related fields of the new departure_id, so the stored values move to the departure record. A dismissal_date is new.
  • hr_org_chart, hr_hourly_cost and hr_homeworking are merged into hr (hr.employee.location now lives in hr).

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 fieldOdoo 20 fieldNote
acc_numberaccount_numberAlso the _rec_name
sanitized_acc_numbersanitized_account_numberStored, computed
acc_holder_nameholder_nameStored, computed, editable
acc_typeaccount_typeComputed selection
bank_id.name / bank_id.bicbank_name / bank_bicCopied 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_registry no longer exists on res.partner or res.company in the 20.0 Community source.
  • res.partner gets additional_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_base is removed together with its l10n_latam.identification.type model.

Inventory, purchase and sales

Unit-of-measure fields are renamed to uom_id on several models:

ModelOdoo 19 fieldOdoo 20 field
stock.moveproduct_uomuom_id
stock.move.lineproduct_uom_iduom_id
stock.warehouse.orderpointproduct_uomuom_id
repair.orderproduct_uomuom_id
purchase.order.lineproduct_uom_iduom_id

sale.order.line and account.move.line keep product_uom_id. Other changes:

  • stock.move.location_final_id is renamed forecasted_location_id.
  • stock.scrap is removed. A scrap is now a stock.move with is_scrap set and optional scrap_reason_tag_ids; the 19 field stock.move.scrap_id is gone.
  • The return wizard (stock.return.picking and its lines) is removed. stock.picking.action_return() creates the return picking directly. sale_stock adds a return.reason model.
  • stock_picking_batch is merged into stock: stock.picking.batch is now in addons/stock.
  • product.template.attribute.exclusion is removed. Exclusions are a Many2many on product.template.attribute.value, excluded_value_ids, limited to values of the same product template.
  • point_of_sale no longer depends on stock_account. Stock handling for POS moves to a new, auto-installed pos_stock module.
  • RFQ alternatives move out of purchase_requisition into a new purchase_alternative module (purchase.alternative.create replaces purchase.requisition.create.alternative).
  • Pricelist rules: the compute_price values percentage and formula are gone. Odoo 20 has discount, markup (shown as Surcharge) and fixed (addons/product/models/product_pricelist_item.py). price_markup is stored as the negative of price_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.value and mail.message.tracking_value_ids move out of mail into a new module, mail_tracking, which is not auto-installed. Without it, mail writes tracked field changes into the message body. A 19 database stores its field-change history as mail_tracking_value rows, so check that old chatter entries still show their changes after an upgrade.
  • website_sale_wishlist and website_sale_comparison are merged into website_sale (product.wishlist and product.attribute.category now live there).
  • The base unit used for unit prices moves from eCommerce to products: website.base.unit becomes product.base.unit, and base_unit_id is defined in product instead of website_sale.
  • website_sale_autocomplete is gone; address autocompletion is in the new website_address_autocomplete, which no longer needs eCommerce.
  • Fleet: fleet.vehicle.log.services.date is replaced by date_from, with a new optional date_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.vote in mail).
  • An llms.txt editor for websites (website.llms, stored in website.llms_txt).
  • Withdrawal requests for online orders (withdrawal.request in website_sale, linked to a sale order).
  • Return reasons on returns (return.reason in sale_stock).
  • POS preparation orders and courses (pos.prep.order, pos.course in point_of_sale).
  • Personal calendars (calendar.calendar, "User Calendar", in calendar).
  • 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_delivery and pos_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:

ScriptRewritesNotes
19.1-00-t-call.pyQWeb XMLt-set inside a t-call body becomes a parameter of the t-call
19.3-00-account-groups.pyChart templatesAccount groups to parent accounts
19.3-00-account-report-foldable.pyAccount report XMLDrops foldable on report lines where it matches the computed value
19.3-00-base64-in-xml.pyXML datatype="base64" file fields to type="bytes"
19.4-00-ir-access.pySecurity CSV/XMLAccess CSV and record rules to ir.access.csv
19.4-00-ormcache-on-transaction.pyPythonregistry.clear_cache to transaction.invalidate_ormcache
19.5-00-tuple-rec_names_search.pyPython_rec_names_search lists to tuples
20.0-00-search-date-filters.pyViewsDrops removed date filter attributes
owl3-migration.pyJS and OWL templatesOWL 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-migration

The 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

Upgrade to Odoo 20

Free test migrations from 15, 16, 17, 18 or 19. No credit card required.

Start your free test migration