Files
callista-odoo-demo/reference/odoo19-field-gotchas.md
T

10 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.

Custom module on v19 — five that cost a build cycle each

  • stock.move.name is gone. _rec_name = 'reference'. Passing name in move vals raises. Set description_picking if you need text on the line.
  • Search views: <group expand="0" string="Group By"> no longer validates. RelaxNG rejects the attributes (ERROR:RELAXNGV:RELAXNG_ERR_NOELEM, reported only as "Invalid view definition"). Use a bare <group> wrapping the group-by <filter>s, as core does.
  • Mixed stored/non-stored fields in one compute → UserWarning: inconsistent 'store', and the non-stored one silently recomputes the stored ones. Give non-stored fields their own compute method.
  • sale.order.line uses product_uom_id in v19 while stock.move still uses product_uom. _action_launch_stock_rule is keyword-only: def _action_launch_stock_rule(self, *, previous_product_uom_qty=False).
  • Suppressing the automatic delivery on confirmation (blanket/call-off orders) is cleanest by overriding sale.order.line._action_launch_stock_rule to skip those lines — no picking is created at all, so nothing has to be cancelled afterwards.

Local Odoo 19 Enterprise in Docker (no Odoo.sh, no sudo needed)

postgres:16 + the official odoo:19 image (it carries every Python dep), but run the mounted 19.0 source, not the image's copy:

entrypoint: ["/bin/bash","-lc","rm -rf /usr/lib/python3/dist-packages/odoo && exec python3 /mnt/odoo/odoo-bin \"$$@\"","--"]
command: ["-c","/etc/odoo/odoo.conf"]

Without the rm, odoo.addons is a namespace package spanning both trees: core loads from the mount while half the addons load from dist-packages — a silent version mix. Verify with a traceback path or python3 -c "import odoo" (should fail).

  • Config keys: http_port (not xmlrpc_port), gevent_port. --without-demo=all is now a boolean — pass --without-demo=True.
  • Driving the build through odoo-bin shell (script on stdin) beats the external API on a local box only: full ORM, no named-argument footguns, and nothing commits unless you call env.cr.commit() — which makes a dry-run of the live demo beat trivially safe (env.cr.rollback()). It needs shell access to the container, so it is not an option on Odoo.sh or Odoo Online; there, JSON-2 remains the only route.
  • After a CLI -u <module>, restart the server container. The upgrade runs in its own process: the DB gets the new fields and views, the long-running web process keeps its old registry. The browser then loads a view referencing a field that server doesn't know → OwlError: "model"."field" field is undefined, reported as an Owl lifecycle crash. odoo-bin shell scripts never see it (fresh registry per process), so the whole test suite can be green while the UI is broken — verify a renamed/added field with fields_get over JSON-2 against the running server, not only through shell.