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>
6.0 KiB
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"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 and go away 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.