Files
callista-odoo-demo/reference/odoo19-field-gotchas.md
T
glenn schrooyenandClaude Opus 5 9876f6ba10 Verify the skill against Odoo 20.0 and note what changed
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>
2026-09-17 17:01:53 +02:00

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.rule records are replaced by ONE security/ir.access.csv (model ir.access, merges ACLs and record rules). Columns: id,name,model_id,group_id/id,operation,domain — operation is a subset of crud, domain is optional. A v19 module shipping ir.model.access.csv will not install on v20.
  • Binary fields over RPC: write accepts {"file_name": ..., "content": ...} on binary fields and web_read no longer returns base64 blobs; bin_size context is gone. Only matters if you upload logos/images — this skill doesn't.
  • RPC db service removed (/xmlrpc/db, /jsonrpc service db). 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_fleet dispatch 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" plus is_storable=True (the old type="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_confirm override.
  • Enable: res.config.settings.account_use_credit_limit = True (+ account_default_credit_limit). Apply via create then execute.
  • Per-customer: res.partner.use_partner_credit_limit = True, credit_limit = <amount>. These are non-stored — you cannot search on use_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 fleet alone = vehicle cost/maintenance; no capacity fields.
  • Install stock_fleet ("Stock Transport"). It adds the dispatch fields to stock.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 → fields weight_capacity, volume_capacity. Create a category with capacity, assign it to the vehicle (fleet.vehicle.category_id), then a batch with that vehicle computes used_weight_percentage.
  • Demo flow: confirm SOs → deliveries → create stock.picking.batch with vehicle_id + picking_ids → read used_weight_percentage.

Currency

  • New databases ship most currencies inactive. Find with search([["name","=","EGP"]], context={"active_test": False}), set active=True, then res.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.quant with inventory_quantity and context={"inventory_mode": True}, then call action_apply_inventory with the same context. The apply may warn harmlessly if already applied; check by reserving (stock.picking.action_assign → state assigned).

State fields are read-only in practice

  • state on sale.order / account.move / stock.picking is writable over RPC and doing so is always wrong: it skips the workflow. Confirming an SO with write creates no delivery and no invoice basis; posting a move with write creates 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_validate can return a wizard dict (stock.backorder.confirmation, immediate transfer) instead of True — create the wizard from res_model/context and call its process button.

RPC helper footgun

  • v19's API is JSON-2 (POST /json/2/<model>/<method>, Authorization: bearer <api key>). /xmlrpc, /xmlrpc/2 and /jsonrpc still work but are deprecated: db service gone in Odoo 20, the rest in Odoo 22.
  • No positional arguments exist. ids go in the ids body key, everything else under the ORM parameter's own name: domain, fields, vals (write), vals_list (create), context, limit. Wrong name = TypeError from 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_post take an id list, search_read replaces search+read in one transaction. get_or_create_many() in odoo_connect.py does the batched get-or-create. A batched create fails 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.