Odoo 20.0 (FINAL) is on GitHub. Every v19 field the skill relies on (group_ids, privilege_id, is_storable, credit-limit fields, stock_fleet dispatch fields) is unchanged; the JSON-2 connector already speaks the v20 API. Real deltas: the RPC db service is gone, custom-module security is a single ir.access.csv, binary fields have a new RPC shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.1 KiB
7.1 KiB
Odoo 19 / 20 (Enterprise) — field names & footguns
Hard-won notes from a real v19 demo build, re-verified against the Odoo 20.0 source (released 2026-09-17). Everything below holds on v20 unless a v20 note says otherwise. Check these before you waste a build cycle. Always confirm the major version first: version()["version"].
Renamed core fields (these break v17/v18 code)
| Concept | v17/18 | v19 |
|---|---|---|
| User's groups | groups_id |
group_ids |
| Group's app category | category_id on res.groups |
removed — use privilege_id (model res.groups.privilege), or just omit |
| (so in security XML) | <field name="groups_id" ...> |
<field name="group_ids" ...> |
A custom module that sets category_id on res.groups or groups_id on res.users will fail to install on v19 with Invalid field ... / Cannot .... Drop the category or use privilege_id.
v20-only changes (verified on the 20.0 branch)
- Custom-module security:
security/ir.model.access.csv+ir.rulerecords are replaced by ONEsecurity/ir.access.csv(modelir.access, merges ACLs and record rules). Columns:id,name,model_id,group_id/id,operation,domain—operationis a subset ofcrud,domainis optional. A v19 module shippingir.model.access.csvwill not install on v20. - Binary fields over RPC:
writeaccepts{"file_name": ..., "content": ...}on binary fields andweb_readno longer returns base64 blobs;bin_sizecontext is gone. Only matters if you upload logos/images — this skill doesn't. - RPC
dbservice removed (/xmlrpc/db,/jsonrpcservicedb). Use/web/database/*or JSON-2 (res.lang,res.country). The connector never used it. - Field renames from v19 (
group_ids,privilege_id,is_storable, credit-limit fields,stock_fleetdispatch fields) are unchanged in v20.
fields_get returns null attributes over RPC (some v19 builds)
fields_get([], {"attributes": ["string","type"]}) may return each field with string=None. The keys (technical names) are still correct — match on keys, not labels. (fields_named() in odoo_connect.py does this.)
Product model (v19)
- Goods that track stock:
type="consu"plusis_storable=True(the oldtype="product"is gone). - Weight:
product.template.weight(kg by default).
Credit limit (Accounting)
- Native behaviour is WARNING ONLY — it does NOT block SO confirmation. A hard block needs a Studio approval rule or a custom
action_confirmoverride. - Enable:
res.config.settings.account_use_credit_limit = True(+account_default_credit_limit). Apply viacreatethenexecute. - Per-customer:
res.partner.use_partner_credit_limit = True,credit_limit = <amount>. These are non-stored — you cannotsearchonuse_partner_credit_limit(Postgres "not stored" error); read it per-record instead. res.partner.credit= current Total Receivable (computed from posted AR moves). Pre-stage it by posting an unpaid customer invoice.
Fleet load-by-weight / Dispatch (v19) — IS native, but gated
- Native
fleetalone = vehicle cost/maintenance; no capacity fields. - Install
stock_fleet("Stock Transport"). It adds the dispatch fields tostock.picking.batch:vehicle_id,vehicle_category_id,vehicle_weight_capacity,used_weight_percentage,estimated_shipping_weight,has_dispatch_management. - Capacity lives on
fleet.vehicle.model.category→ fieldsweight_capacity,volume_capacity. Create a category with capacity, assign it to the vehicle (fleet.vehicle.category_id), then a batch with that vehicle computesused_weight_percentage. - Demo flow: confirm SOs → deliveries → create
stock.picking.batchwithvehicle_id+picking_ids→ readused_weight_percentage.
Currency
- New databases ship most currencies inactive. Find with
search([["name","=","EGP"]], context={"active_test": False}), setactive=True, thenres.company.currency_id. - Change company currency BEFORE posting any invoices/journal entries — Odoo blocks the switch once entries exist.
Stock seeding (so deliveries go Ready)
- Create
stock.quantwithinventory_quantityandcontext={"inventory_mode": True}, then callaction_apply_inventorywith the same context. The apply may warn harmlessly if already applied; check by reserving (stock.picking.action_assign→ stateassigned).
State fields are read-only in practice
stateonsale.order/account.move/stock.pickingis writable over RPC and doing so is always wrong: it skips the workflow. Confirming an SO withwritecreates no delivery and no invoice basis; posting a move withwritecreates no journal entries and no sequence number.- Use the button method:
action_confirm,action_post/button_draft/button_cancel,action_assign+button_validate,button_confirm(PO). stock.picking.button_validatecan return a wizard dict (stock.backorder.confirmation, immediate transfer) instead ofTrue— create the wizard fromres_model/contextand call itsprocessbutton.
RPC helper footgun
- v19's API is JSON-2 (
POST /json/2/<model>/<method>,Authorization: bearer <api key>)./xmlrpc,/xmlrpc/2and/jsonrpcstill work but are deprecated:dbservice gone in Odoo 20, the rest in Odoo 22. - No positional arguments exist. ids go in the
idsbody key, everything else under the ORM parameter's own name:domain,fields,vals(write),vals_list(create),context,limit. Wrong name =TypeErrorfrom the server, not a silent misread — but guessing still costs a round trip. Every model+method signature for YOUR database is published at<ODOO_URL>/doc. - The old execute_kw footgun (a context dict passed positionally landing in
offset→psycopg2 can't adapt type 'dict') is gone with the endpoint. If you see it, you are on legacy code. - One call, one transaction. Committed on success, discarded on error, and nothing chains across calls. Prefer a single method that does the whole job (
search_read,action_confirm) over a sequence you orchestrate client-side. - API keys expire — three months maximum, one day if you generated it interactively. Check the expiry before demo day.
- Odoo Online exposes the external API on Custom plans only (not One App Free, not Standard).
- Batch by widening calls, not by bundling them. JSON-2 has no multi-call envelope, but every ORM method is set-based:
create(vals_list=[...])takes a list and returns ids in order,write/read/action_posttake an id list,search_readreplaces search+read in one transaction.get_or_create_many()inodoo_connect.pydoes the batched get-or-create. A batchedcreatefails as a unit — keep the loop where you need per-record error isolation.
Module install over RPC
x("ir.module.module","button_immediate_install", ids). Heavy — let it run minutes (x()defaults to a 600s timeout for this reason). On a failed XML-data load it rolls back cleanly (module stays uninstalled); fix and retry.- After deploying a custom module to git (Odoo.sh), wait for the rebuild, then
x("ir.module.module","update_list")and search for it before installing.