Odoo compatibility

Metrilens runs on Odoo 17, 18 and 19.

That is the whole answer for most people. If you want to see the work behind it, the rest of this page is the engineering matrix itself — published as it is maintained, including the parts still marked unconfirmed.

17.0, 18.0 and 19.0

All three are verified. Not from release notes alone — the checks were made against the odoo/odoo source branches, and the HR section additionally against a live Odoo 19.0 Enterprise instance.

It connects the way Odoo expects

Metrilens reads over Odoo's JSON-RPC endpoint with an API key. That path works unchanged on 17, 18 and 19. Odoo has flagged it as deprecated with removal announced for a much later version, so nothing about your install has to change to use it today.

Six differences that matter

Between 17 and 19 Odoo made six changes a reporting tool has to know about — a removed product type, a purchase state that became a flag, the removal of hr.contract. The matrix names each one, the version it landed in, and what it requires. Two of them are branched on your server version while the dashboard runs.

If you run Odoo 16 or older

Metrilens signs in with an Odoo API key, and Odoo only introduced API keys in version 14 — below that there is no supported way to connect. Versions 14 to 16 are not on the verified list: parts of the product will work, but we have not checked them and do not claim them. Tell us which version you are on before you buy and we will say so plainly.

The verified matrix

From here on this is the internal document, rendered unedited. It is written for engineers, it cites the exact source files behind each claim, and it flags what could not be confirmed. That is deliberate: a compatibility promise you cannot check is not worth much.

This file is published. The public page /compatibility is generated from it at build time (../src/app/compatibility/page.tsx) — edit it here and nowhere else, and read it as customer-facing text. The former standalone rendering docs/odoo-compat-matrix.html was retired because it had already gone stale (it never got the HR section).
Verified July 2026 against primary source: odoo/odoo GitHub branches 17.0, 18.0, 19.0 (raw source files), official release notes and developer docs. Every non-trivial claim is cited in the Sources section. Items that could not be confirmed from a primary source are flagged UNCONFIRMED.

TL;DR — the breaking changes you must handle

#Breaking changeVersionAction
1Storable filter: type='product'is_storable=True18Branch on version when filtering storable products
2<tree> view tag → <list> (hard removed, raises ValueError)18Rename all <tree> roots to <list>
3purchase.order state='done'locked boolean19Query state='done' OR locked=True; confirmed PO is state='purchase'
4purchase.menu_purchase_report never existedallUse purchase.purchase_report_main
5read_group() deprecated (still works, warns)19Keep for now; migrate to _read_group before ~v22
6hr.contract removed → hr.version (no state, contract_date_* dates, many rows per employee)19Go through hr-compat.ts; never count version rows as headcount

Everything else in the audited surface is stable 17 → 19. The read-only web-session JSON-RPC path used by the SaaS dashboard is safe across all three versions.


PRODUCT / INVENTORY

Item171819Changed inFix
product.type / storable detectiontypeconsu,service; stock adds ('product','Storable'); also detailed_typetypeconsu("Goods"),service,combo; 'product' REMOVED; stock adds is_storable Boolean; detailed_type removedsame as 1818Storable filter — 17: [('type','=','product')]; 18/19: [('is_storable','=',True)]
product standard_price / list_price / default_code / categ_idokokokStable
stock.quant quantity + location_id.usage=='internal'okokokStable
stock.move state=='done' + dateokokokStable
stock.warehouse.orderpoint product_min_qty/product_max_qtyokokokStable

MRP

Item171819Changed inFix
mrp.bom (product_tmpl_id,product_id,product_qty,active,type=normal/phantom) + mrp.bom.line (bom_id,product_id,product_qty)okokokStable
mrp.bom._bom_find()_bom_find(self, products, picking_type=None, company_id=False, bom_type=False)defaultdict keyed by product record → mrp.bom recordsetbyte-identicalbyte-identicalStable. Returns dict-like; value is empty recordset when none (never None/False)

SALES

Item171819Changed inFix
sale.order statedraft,sent,sale,cancel + locked boolsamesame'done' removed at/before 17 → locked boolNever query state='done'; confirmed order = state='sale', locked = locked=True
sale.order date_order/amount_total/amount_untaxed/user_id/partner_id/currency_idokokokStable
sale.order.line price_subtotal/price_unit/discount/product_uom_qty/display_typeokokokStable
sale_margin: margin/purchase_price (+margin_percent)okokokModule name & fields stable; present only if sale_margin installed

ACCOUNTING

Item171819Changed inFix
account.account account_type (asset_receivable,liability_payable,…)full setidentical to 17adds expense_other19 (additive only)Existing values unchanged; add expense_other only if enumerating expense types on 19
account.move.line amount_residual/date_maturity/parent_state/partner_id/company_id/dateokokokStable
account.move invoice_date/move_type/stateokokokStable (state draft/posted/cancel)

PURCHASE

Item171819Changed inFix
purchase.order statedraft,sent,to approve,purchase,done,cancelsame (incl. done)done REMOVED: draft,sent,to approve,purchase,cancel + locked bool19 (PR #188374)17/18: done valid. 19: state='done' matches nothing — use locked=True. Cross-version: state='done' OR locked=True. PO = state='purchase'
purchase.order amount_total/date_approve/date_orderokokokStable
stock.picking purchase_id / picking_type_code=='incoming' / date_done / scheduled_dateokokokStable (purchase_id from purchase_stock)

HR

Verified July 2026 against the live 19.0+e reference instance (fields_get + search_count), not only the source branches.

Item171819Changed inFix
hr.contractokokmodel REMOVEDObject hr.contract doesn't exist19Employment terms live on hr.version, one row per version of an employee's terms. Branch via contractModel() in ../src/lib/odoo/hr-compat.ts
contract statedraft,open,close,cancelsamefield does not exist on hr.version19"In force" = contract_date_start set and reached AND (contract_date_end unset OR still ahead) — the rule behind Odoo's own is_in_contract
contract start/end datesdate_start / date_endsamecontract_date_start / contract_date_end (stored). date_start/date_end still exist as unstored aliases19Unstored aliases work in a domain but raise "Cannot convert hr.version.date_end to SQL because it is not stored" in order. Only ever name the contract_* fields
is_in_contractexists, unstored19Not usable in a domain at all; replicate the rule instead
row ↔ personone open contract per employeesamemany versions per employee (reference instance: 17 rows / 12 employees)19Never count rows as headcount — read_group by employee_id (countEmployeesWithContract())
hr.version name(contract name, set)sameoften false19Fall back to the employee name (contractLabel())
hr.employee (active,department_id,job_id,create_date)okokokStable
hr.leave (state,date_from,date_to,number_of_days)okokokStable (hr.holidays only pre-12)

VIEWS

Item171819Changed inFix
<tree> list tagvalid (normalized to list internally)HARD REMOVED<list> requiredHARD REMOVED18Rename <tree><list>. No backward compat: <tree> root raises ValueError: Wrong value for ir.ui.view.type: 'tree'
<form>/<pivot>/<graph>/<search>okokokStable
<header><button> in listokokokSupported all three
column_invisible/optional/decoration-*okokokcolumn_invisible since 17Use column_invisible (plain invisible hides only the cell)
attrs=/states=REMOVEDremovedremoved17Use direct expressions: invisible="state in ('draft','cancel')", readonly=, required=
Item171819Changed inFix
sale.menu_sale_reportokokokOK
account.menu_finance_reportsokokokOK
purchase.menu_purchase_reportdoes not existdoes not existdoes not existn/aWrong id in all versions — use purchase.purchase_report_main
stock.menu_warehouse_reportokokokOK
mrp.menu_mrp_reportingokokokOK
All 10 group xmlids (sales_team.group_sale_salesman/manager; account.group_account_readonly/manager; purchase.group_purchase_user/manager; stock.group_stock_user/manager; mrp.group_mrp_user/manager)okokokAll confirmed present in every branch

ORM

Item171819Changed inFix
read_group() (public)worksworksworks but @api.deprecated (warns)19Still functional in 19 (compat over _read_group). Keep read_group for a 17–19 tool — only dict-returning group API common to all three. Migrate before ~v22
_read_group()(domain, groupby, aggregates, having, offset, limit, order) → list of tuplessamesame (type-annotated)rewritten in 17Stable 17→19; returns tuples not dicts; private
create([{…}]) batchok (@api.model_create_multi)okokv12No change; single dict still accepted
SQL view _auto=False + init() + tools.sql.drop_view_if_existsokokokPattern valid all three; drop_view_if_exists(cr, name) unchanged

MANIFEST

Item171819Changed inFix
__manifest__.py keys (name/version/depends/data/assets/demo/installable/application/auto_install/license/*_hook)stablestablestable (internals refactored to "Manifest API")No key add/remove/rename observed diffing 17 vs 19
version string17.0.x.y.z18.0.x.y.z19.0.x.y.zPrefix with Odoo serie for custom modules
assets bundle formatbundle → [paths] dictsamesameNo format change

EXTERNAL API (read-only JSON-RPC SaaS)

Item171819Changed inFix
/web/session/authenticate + /web/dataset/call_kw (search_read) — web session pathokokok — not deprecatedtype='json'type='jsonrpc' controller rename in 19 (transparent to clients)Current path. Fully supported 17–19 self-hosted. No client change needed
/jsonrpc (external RPC endpoint)okokok but DEPRECATED19Deprecated; official removal on-prem v22 (fall 2028) / Online 21.1 (winter 2027). Some blogs say "removed in 20" — official docs say 22; trust primary source
/xmlrpc,/xmlrpc/2 (execute_kw)okokok but DEPRECATED19Same timeline as /jsonrpc
New /json/2 REST API (Bearer API-key)new in 1919Optional modern alternative; API keys default max lifetime 90 days for non-admins

Auth/2FA note: No confirmed change breaking programmatic read via the web-session path in 18/19. API keys remain recommended; 2FA affects interactive login, not key/session RPC. Low risk, but verify against your specific Odoo Online plan.


Odoo 20 Outlook

Status as of July 2026: does NOT exist yet — roadmap only. Verified from primary source:

  • No 20.0 branch on GitHub. git ls-remote shows only 17.0, 18.0, 19.0, master. raw.githubusercontent.com/odoo/odoo/20.0/odoo/release.pyHTTP 404.
  • 19.0 is FINAL: release.py = (19, 0, 0, FINAL, 0, '').
  • Official release-notes page tops out at 19.3 (May 2026) — the Online incremental cadence. No Odoo 20 entry.

Publicly known (secondary / soft):

  • Odoo 20 roadmap presented April 2026 (Luc Nailis, Odoo PO) — explicitly caveated "things we will maybe do."
  • Expected unveiling at Odoo Experience 2026, Brussels, ~Sept 24–26 2026; GA typically Oct–Nov 2026.
  • Themes: heavy AI automation, accounting/POS/payroll/website, mobile redesign. No confirmed breaking schema/API changes for the audited items.

Pre-planning for v20:

  1. locked boolean is the durable representation of "done/locked" — v19 finished it for purchase.
  2. Migrate read_group_read_group/formatted_read_group opportunistically.
  3. <tree> stays removed; standardize on <list>.
  4. Plan a /json/2 migration path off /jsonrpc+/xmlrpc (removal 2027 Online / 2028 on-prem).

UNCONFIRMED — verify before relying

  • formatted_read_group exact signature/location in 19 — referenced in the deprecation message but the def was not located. Check: grep -rn "def formatted_read_group" on a real 19.0 checkout. Does not affect a read-only tool.
  • /jsonrpc removal version — official docs say v22 / Online 21.1; some third-party blogs say "Odoo 20." Sources disagree; primary source (docs) wins.
  • New optional manifest keys in 19 — confirmed no existing keys changed; did not exhaustively rule out a new optional key sale doesn't happen to use.
  • 2FA/API-key impact on RPC read in 18/19 — no breaking change found, not exhaustively confirmed against every Odoo Online plan variant.

Sources

Primary (raw GitHub, branches 17.0 / 18.0 / 19.0):

  • addons/product/models/product_template.py, product_product.py; addons/stock/models/product.py, stock_quant.py, stock_move.py, stock_orderpoint.py
  • addons/mrp/models/mrp_bom.py
  • addons/sale/models/sale_order.py, sale_order_line.py; addons/sale_margin/models/sale_order_line.py
  • addons/account/models/account_account.py, account_move.py, account_move_line.py
  • addons/purchase/models/purchase_order.py; addons/purchase_stock/models/stock.py; addons/stock/models/stock_picking.py
  • odoo/addons/base/models/ir_ui_view.py; addons/*/views/*.xml, addons/*/security/*_security.xml, menu XMLs
  • odoo/orm/models.py (19), odoo/models.py (17/18); odoo/tools/sql.py; addons/sale/report/sale_report.py; addons/sale/__manifest__.py; odoo/release.py
  • PR https://github.com/odoo/odoo/pull/188374 (purchase done→locked)

Official docs / release notes:

Secondary (tree→list hard error, attrs/states, Odoo 20 roadmap — soft):

Generated 2026-07-07.

The other question buyers ask first: What it cannot do.