Files
glenn schrooyenandClaude Opus 5 93edf9296e Use the real Odoo 19 external API (JSON-2), and batch calls
The previous commit moved to /jsonrpc, which the v19 docs list as deprecated
alongside /xmlrpc — both go away in Odoo 22. The actual v19 external API is
JSON-2: POST /json/2/<model>/<method>, API key as a bearer token.

Three consequences, all of which change calling code:
  - no uid and no password; UID now comes from res.users/context_get
  - every argument is named, positional args do not exist, so x() becomes
    x(model, method, ids=None, **kwargs) and callers pass domain=/vals=/fields=
  - one call is one transaction and nothing chains, which is the mechanical
    reason state changes must go through a button method

Batching: no multi-call envelope exists, so batching means widening a call, not
bundling calls. Added get_or_create_many() (one search_read + one create for a
whole set) and put build_demo.py's product, customer, vehicle, invoice and
headroom loops through set-based calls.

test_odoo_connect.py checks the call shape and the batching offline, with a
faked urlopen — none of this can be exercised without a live instance otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 02:53:03 +02:00

6.0 KiB

Odoo 19 (Enterprise) — field names & footguns

Hard-won notes from a real v19 demo build. 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.

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 and go away 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 offsetpsycopg2 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.