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>
This commit is contained in:
glenn schrooyen
2026-08-09 02:53:03 +02:00
co-authored by Claude Opus 5
parent 3fd2c51a3d
commit 93edf9296e
6 changed files with 295 additions and 129 deletions
+58 -10
View File
@@ -1,6 +1,6 @@
---
name: callista-odoo-demo
description: Build a complete, client-tailored Odoo demo end to end — assembles the discovery brief from any source (transcript, notes, email, Knowcap, or a live interview), researches native-vs-custom per hero feature, stands up a LIVE Odoo database over JSON-RPC (modules + realistic data + working heroes), tests it, and generates the sales docs and the post-demo follow-through set (objection sheet, scoped quote, timeline, SOW). Use when prepping an Odoo sales demo, POC, or proof-of-value, or when writing the follow-up after one. Triggers: "build an Odoo demo", "demo DB for <client>", "Odoo proof of concept", "stand up a demo before the meeting", "write the follow-up after the demo".
description: Build a complete, client-tailored Odoo demo end to end — assembles the discovery brief from any source (transcript, notes, email, Knowcap, or a live interview), researches native-vs-custom per hero feature, stands up a LIVE Odoo database over the Odoo 19 JSON-2 API (modules + realistic data + working heroes), tests it, and generates the sales docs and the post-demo follow-through set (objection sheet, scoped quote, timeline, SOW). Use when prepping an Odoo sales demo, POC, or proof-of-value, or when writing the follow-up after one. Triggers: "build an Odoo demo", "demo DB for <client>", "Odoo proof of concept", "stand up a demo before the meeting", "write the follow-up after the demo".
---
# Callista Odoo Demo
@@ -73,7 +73,7 @@ Everything else is supporting flow.
Odoo apps in scope, plus any vertical modules routed by an extending skill.
## Odoo instance
URL, database, login, API key. Ask if absent — do not guess.
URL, database, API key (JSON-2 needs no login). Ask if absent — do not guess.
UI language + `l10n_*` localisation to install.
## Demo audience
@@ -96,7 +96,7 @@ casually, because extending skills point at these names.
*Instance prep* and *client research* are independent: the install list comes from the
brief (plus any routing overlay), while research feeds hero *building*, which happens
after both. Installing over JSON-RPC is minutes of waiting that produces log spew nobody
after both. Installing over RPC is minutes of waiting that produces log spew nobody
needs in context. Run them as two parallel subagents launched in **one** message:
**Agent 1 — instance prep.** Give it the Odoo credentials and the module list. It sets
@@ -129,10 +129,51 @@ An extending skill may insert its own tier ahead of native — see [Extension po
### Connect
Use `scripts/odoo_connect.py` (JSON-RPC — the recommended API from Odoo 19 on). Authenticate,
print `version()["server_version"]` to confirm the major version (field names differ across
versions — see `reference/odoo19-field-gotchas.md`). All build steps go through the
`x(model, method, *args, **kwargs)` helper.
Use `scripts/odoo_connect.py`. It speaks **JSON-2** (`POST /json/2/<model>/<method>`, API key
as a bearer token) — Odoo 19's external API. `/xmlrpc`, `/xmlrpc/2` and `/jsonrpc` are all
deprecated and disappear in Odoo 22; do not write execute_kw calls.
Print `version()["version"]` to confirm the major version (field names differ across versions
— see `reference/odoo19-field-gotchas.md`). All build steps go through the
`x(model, method, ids=None, **kwargs)` helper. Three JSON-2 facts to build around:
- **Every argument is named. There are no positional arguments.** Record ids go in `ids`;
everything else goes under the ORM method's own parameter name —
`x("res.partner", "search", domain=[...], limit=5)`,
`x("res.partner", "write", ids, vals={...})`, `x(model, "create", vals_list={...})`.
Guessing the parameter name is the new version-assumption trap: the database publishes its
own signatures at `<ODOO_URL>/doc` — read it rather than guess.
- **No uid, no password.** The API key identifies the user; `UID` comes from
`res.users/context_get`. Keys expire — three months maximum, and a demo built on a
one-day key stops working the morning of the demo.
- **Each call is its own SQL transaction**, committed on success, discarded on error. Nothing
chains. This is exactly why the next step exists.
Odoo Online serves the external API on **Custom** plans only — not One App Free, not Standard.
Check this before promising a live build on a client's own trial instance.
### Batch every call you can
**One call over N records, never N calls over one record.** Odoo's ORM methods are all
set-based; the round trip is the expensive part, and a build that loops is the difference
between a demo standing up in one minute and in twenty.
| Loop of N calls | The one call that replaces it |
|---|---|
| `create` per record | `create(vals_list=[{...}, {...}, ...])` → list of ids, in order |
| `write` per record, same values | `write(ids, vals={...})` — ids is a list |
| `search` then `read` | `search_read(domain=..., fields=[...])` — also one transaction, so no TOCTOU |
| `read` per id | `read(ids, fields=[...])` |
| `action_confirm` / `action_post` per record | the same button over the whole id list |
| get-or-create per record | `get_or_create_many(model, key_field, rows)` — one search_read + one create |
There is **no multi-call batch envelope** in JSON-2 — you cannot pack unrelated calls into
one HTTP request, and each call commits separately. Batching means widening a call, not
bundling calls. Where records genuinely differ (different `vals` per record), a loop is
correct; keep it and move on.
Two places not to batch: anything whose per-record failure you need to isolate (a batched
`create` fails as a unit, so one bad row loses the set), and the live demo click itself.
### Move records with buttons, never with fields
@@ -147,14 +188,19 @@ button calls — not through `write` on `state` or any other status field:
| `x("purchase.order", "button_confirm", [ids])` | `write(ids, {"state": "purchase"})` |
| `x("account.move", "button_draft", [ids])` before cancel | `write(ids, {"state": "draft"})` |
This is also the only way to stay consistent: every JSON-2 call is a separate transaction, so
a hand-rolled sequence of writes can be interrupted halfway by a concurrent change and leave a
half-confirmed record. The button method does the whole thing in one transaction — all of it
commits, or none of it does.
Writing the field skips everything the button does — no delivery order, no journal entries,
no sequence number, no stock moves — so the record *looks* confirmed and every downstream
screen in the demo is empty. It also silently skips the very validations a hero feature is
meant to demonstrate.
Don't guess a method name: `fields_named()` finds fields, but for methods read the button off
the form view (`x(model, "get_views", [[[False, "form"]]], ...)`) or check the model in the
docs. `button_validate` on a picking may return a wizard dict (immediate transfer, backorder)
the form view (`x(model, "get_views", views=[[False, "form"]])`) or read the database's own
signatures at `<ODOO_URL>/doc`. `button_validate` on a picking may return a wizard dict (immediate transfer, backorder)
— call the wizard's own button rather than treating the dict as success.
### Install modules
@@ -325,6 +371,7 @@ Rules for the boundary:
- **Research before building.** Version assumptions are the #1 source of wasted effort.
- **Buttons, not fields.** Never advance a record's state with `write` — call the button
method (see *Move records with buttons, never with fields*).
- **Batch.** One call over N records, never N calls over one (see *Batch every call you can*).
- **Don't hard-code the client.** Everything client-specific lives in `demo_config.json`.
- **Currency before invoices. Weight before fleet. Pre-stage before the live click.** Order matters.
- Never hand over an environment holding test records. Purge them and read back the counts;
@@ -337,8 +384,9 @@ Rules for the boundary:
## Files
- `scripts/odoo_connect.py` — JSON-RPC connector + `x()` helper (correct context-kwarg passing).
- `scripts/odoo_connect.py` — JSON-2 connector + `x()` helper (named-argument call shape).
- `scripts/build_demo.py` — config-driven data + hero builder (template to adapt per engagement).
- `scripts/test_odoo_connect.py` — offline self-check: JSON-2 call shape + batched get-or-create.
- `demo_config.example.json` — the shape of a per-client config.
- `reference/odoo19-field-gotchas.md` — version-specific field names + footguns learned the hard way.
- `templates/` — doc templates (brand-themed, `{{placeholder}}` driven).