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>
394 lines
21 KiB
Markdown
394 lines
21 KiB
Markdown
---
|
||
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 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
|
||
|
||
Turn a discovery call into a working Odoo demo, then into the documents that sell it.
|
||
|
||
Three phases. **Discovery brief** → **Build** → **Follow-through**. Each is usable alone:
|
||
a brief without a build is still a brief, and Follow-through runs days after the demo.
|
||
|
||
> Fork of [Smetools/odoo-demo-architect](https://github.com/Smetools/odoo-demo-architect)
|
||
> (MIT). Diverges in three ways: discovery is source-agnostic rather than Knowcap-bound,
|
||
> the build forks two parallel agents, and Follow-through is new. See `UPSTREAM.md`.
|
||
|
||
**Vertical skills extend this one** — see [Extension points](#extension-points). If a
|
||
vertical skill invoked you, apply its overlays; do not make it restate the pipeline.
|
||
|
||
---
|
||
|
||
## Phase A — Discovery brief
|
||
|
||
Goal: `discovery_brief.md` in the six-section shape the rest of the pipeline consumes.
|
||
|
||
**Source, in order of preference:**
|
||
1. A transcript, meeting notes, or email thread — read it.
|
||
2. Knowcap over MCP, if available and the user wants it (`get_source_transcriptions`,
|
||
after locating the source with `list_sources` / `search_memories`).
|
||
3. Nothing written down → interview the user.
|
||
|
||
### Intake — infer first, ask only the gaps
|
||
|
||
Before asking anything, **look**: glob the working directory for a transcript, notes,
|
||
brief, or `.md`/`.txt`/`.docx` that plausibly is the source, and read it. Then answer
|
||
each item below from what you read. Only the items you genuinely cannot answer go to
|
||
the user, in **one** `AskUserQuestion` batch — never a serial interrogation, never a
|
||
question whose answer is sitting in the file you just read.
|
||
|
||
| Item | Infer from | Ask only if |
|
||
|---|---|---|
|
||
| **Source brief location** | A single obvious candidate file in the working directory | Zero or several candidates — ask which, or paste |
|
||
| **Odoo UI language** | Language the brief is written in; client country; `l10n_*` in scope | Brief is English but the client clearly isn't anglophone — ask (nl_BE / fr_BE / en_US / other) |
|
||
| **Client website / company info** | URL or company name in the brief — fetch the site, mine it for what they build or sell, size, regions, branding | No name or URL anywhere in the source |
|
||
| **Odoo instance** | `demo_config.json`, prior session, the brief | Not found — always ask, never guess a URL or API key |
|
||
| **Demo audience** | Attendee list in a transcript header or email thread | Source is notes with no names |
|
||
| **Rate card** | A rates file in the project | Only needed at Phase C — don't ask now |
|
||
|
||
Anything still missing after that batch: write it into the brief as `(unknown)` and
|
||
carry on. One unknown does not block building the demo.
|
||
|
||
If nothing written down exists at all, the interview replaces the table — still one
|
||
batch: who the client is, what they sell or build, how they quote today, how they
|
||
invoice, the top 3 complaints they voiced, who is in the room.
|
||
|
||
**Write `discovery_brief.md` with these six sections and no others:**
|
||
|
||
```markdown
|
||
## Business shape
|
||
Entities, product/customer types, volumes, shared financials.
|
||
Company name, website, country, what they actually do — from the site if you fetched it.
|
||
Watch for "it's a group" that is really ONE company in Odoo — confirm whether
|
||
chart-of-accounts, P&L and warehouse are shared.
|
||
|
||
## Top pains
|
||
3–5 items. Direct client quotes where you have them. Mark anything you inferred as (inferred).
|
||
|
||
## Hero features
|
||
1–2. Each names the pain it kills and the decision-maker it lands on.
|
||
Everything else is supporting flow.
|
||
|
||
## Phase 1 scope
|
||
Odoo apps in scope, plus any vertical modules routed by an extending skill.
|
||
|
||
## Odoo instance
|
||
URL, database, API key (JSON-2 needs no login). Ask if absent — do not guess.
|
||
UI language + `l10n_*` localisation to install.
|
||
|
||
## Demo audience
|
||
Who is in the room and what each one needs to hear.
|
||
```
|
||
|
||
Never invent a pain quote. An inferred pain marked `(inferred)` is fine; a fabricated
|
||
quote read back to the client in the demo is not.
|
||
|
||
Show the brief to the user and get a yes before Phase B.
|
||
|
||
---
|
||
|
||
## Phase B — Build
|
||
|
||
Steps are **named, not numbered** — reference them by name, and never rename a step
|
||
casually, because extending skills point at these names.
|
||
|
||
### Fork the slow half
|
||
|
||
*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 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
|
||
company, currency, UoM and UI language, installs the localisation, then the modules,
|
||
then walks Settings for behaviours that default off. It loads **no data** — that is the
|
||
join step's job. Returns: what installed, what failed, the settings it changed.
|
||
|
||
**Agent 2 — client research.** Give it the brief. It fetches the client's website and
|
||
researches each hero feature (see *Research native-vs-custom*). Returns: per hero, the
|
||
verdict and the click-path if the capability already exists.
|
||
|
||
**Join.** Both done → install anything research turned up that prep didn't know about
|
||
(incremental, cheap) → *Load demo data* → *Build heroes* → *Test heroes*.
|
||
|
||
Two agents, not five. Do not fan out further — hero building is sequential against one
|
||
database, and parallel writes will fight.
|
||
|
||
### Research native-vs-custom
|
||
|
||
For EACH hero feature, verify whether Odoo does it natively, no-code, or needs custom
|
||
code — for the client's **exact Odoo version**. Use the Perplexity MCP (`perplexity_ask`)
|
||
or web search. Ask precisely, e.g. *"In Odoo <ver>, is there a native hard-block (not
|
||
warning) on SO confirmation over credit limit?"* Capture the verdict + citations.
|
||
|
||
This step repeatedly overturns assumptions — credit limit is **warning-only** natively;
|
||
v19 fleet dispatch IS native but only via `stock_fleet`. Version assumptions are the #1
|
||
source of wasted effort.
|
||
|
||
An extending skill may insert its own tier ahead of native — see [Extension points](#extension-points).
|
||
|
||
### Connect
|
||
|
||
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
|
||
|
||
**Every state change goes through the model's own button method** — the same method the UI
|
||
button calls — not through `write` on `state` or any other status field:
|
||
|
||
| Do | Never |
|
||
|---|---|
|
||
| `x("sale.order", "action_confirm", [ids])` | `write(ids, {"state": "sale"})` |
|
||
| `x("account.move", "action_post", [ids])` | `write(ids, {"state": "posted"})` |
|
||
| `x("stock.picking", "action_assign", [ids])` then `"button_validate"` | `write(ids, {"state": "done"})` |
|
||
| `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", 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
|
||
|
||
`button_immediate_install` on the `ir.module.module` ids. Common set: `sale_management`,
|
||
`stock`, `account_accountant`, `stock_delivery`, `stock_picking_batch`, `fleet`,
|
||
`web_studio`, `base_automation`, `contacts`. For fleet load-by-weight on v19 also install
|
||
`stock_fleet` (Stock Transport — adds vehicle capacity + batch dispatch). Module installs
|
||
are heavy; allow several minutes.
|
||
|
||
### Load demo data
|
||
|
||
Industry-appropriate, minimal but believable, generated from the brief — the client's own
|
||
partners, products, projects and orders, staged so each hero has a record to fire on.
|
||
Drive it from a `demo_config.json` (see `demo_config.example.json`) and
|
||
`scripts/build_demo.py`.
|
||
|
||
**`build_demo.py` is a per-engagement template, not an engine.** It implements one shape
|
||
(products, fleet, credit limits, prestaged receivables) and reads every key with
|
||
`cfg.get()`, so anything it does not implement would otherwise be skipped in silence — a
|
||
`DONE` over an empty database. It now refuses to run on config keys it cannot build, listing
|
||
them; extend the script, or drop the keys. `DEMO_ALLOW_UNKNOWN_KEYS=1` overrides for a
|
||
deliberate partial run. Extend `KNOWN_KEYS` whenever you add a block.
|
||
|
||
Rules:
|
||
|
||
- **Get-or-create by name** so re-runs are idempotent.
|
||
- Set the right **currency** first (activate it with `context={'active_test': False}`, set
|
||
on `res.company`) — BEFORE posting any invoices. Currency can't change once journal
|
||
entries exist.
|
||
- Give products a **weight** if fleet is in scope. Give customers **credit limits**
|
||
(`use_partner_credit_limit=True`, `credit_limit=...`) if credit is in scope.
|
||
- **Pre-stage state** so heroes fire live: e.g. post an unpaid invoice putting a
|
||
customer's receivable JUST under their limit, so a live order tips them over. Keep one
|
||
demo quotation in **draft** for the live click.
|
||
- Seed stock with `inventory_mode` context so deliveries can reserve (go Ready).
|
||
|
||
### Build heroes
|
||
|
||
- **Prefer no-code:** server actions / automated rules (`base.automation`) / Studio.
|
||
Portable to any Odoo, no deploy.
|
||
- **Custom module only if needed:** generate a minimal module, push to the instance's git
|
||
(Odoo.sh) or hand it to the user to deploy, then install + test. Keep modules tiny and
|
||
version-correct.
|
||
- Example hero (credit hard-block + manager override): override `sale.order.action_confirm`
|
||
to raise `UserError` when `partner.credit + amount_total > credit_limit` unless the user
|
||
is in an approver group; add an "Override" button gated to that group.
|
||
|
||
### Test heroes
|
||
|
||
**Mandatory.** A demo hero that fails live loses the deal. Trigger each hero, assert the
|
||
block fires, assert the override path works. Print a verification log with actual numbers
|
||
(e.g. "credit block raised at 56,379 > 50,000; override confirmed").
|
||
|
||
Test against a **throwaway** partner named `ZZ Test…`, never the client's own records — a
|
||
hero you tested is a hero you consumed, and the demo needs its first click to be the first
|
||
click.
|
||
|
||
### Purge test residue
|
||
|
||
**A demo environment ships with zero test residue.** Debugging a chain takes several runs,
|
||
each leaving a full trail; "it's cancelled and the partner is archived" is not clean —
|
||
those records still list on screen. Write the purge as a function in the test script and
|
||
run it until the counts are zero, not as a one-off you improvise at the end.
|
||
|
||
Odoo blocks most deletion unless you go in dependency order, leaves last. An extending
|
||
skill supplies the exact chain for its own models; the generic order is:
|
||
|
||
```
|
||
transient wizard rows → downstream documents (cancelled only, newest first)
|
||
→ invoices → time entries → tasks → projects → sale orders → the partner
|
||
```
|
||
|
||
The traps, each of which costs a debugging round:
|
||
|
||
- `search` will not find the partner once archived — pass `context={"active_test": False}`
|
||
or the purge silently no-ops and reports success.
|
||
- Invoices must go to draft before cancel; period-sequenced documents cancel newest-first only.
|
||
- **Delete the partner last, and only once nothing references it.** Deleting it first nulls
|
||
`partner_id` on whatever survived, so a `partner_id`-based purge can no longer see those
|
||
records and a `partner_id`-based assertion reports clean over an environment that isn't.
|
||
Sweep by name as well, and assert on both.
|
||
- Anything the tests **copy** inherits the source record's customer, not the throwaway one.
|
||
Repoint copies at the throwaway partner or they escape the purge.
|
||
- Make every step best-effort with its own `try`, and **print what it kept** — a silent
|
||
purge that skipped nine records reads exactly like one that worked.
|
||
|
||
Finish by reading back the counts of every model you touched into the verification log.
|
||
"No test residue" is a claim; the counts are the evidence.
|
||
|
||
### Sanity-check the staged numbers
|
||
|
||
A chain that passes every assertion can still look broken on screen. Check the staged data
|
||
reads plausibly: quantities consumed well under quantities ordered (over 100% makes any
|
||
progress or delivery view look wrong), costs under selling prices, dates in the past.
|
||
Derive staged quantities from the ordered quantity rather than hard-coding them, so they
|
||
cannot drift when a formula changes.
|
||
|
||
### Sales docs
|
||
|
||
From `templates/` (brand-themed, `{{placeholder}}`-driven), produce three self-contained
|
||
HTML files filled with the client's data:
|
||
|
||
- **run-of-show** — beats with exact click-paths + what to say.
|
||
- **one-pager** — before/after, what's automated, investment, path to go-live; branded for
|
||
the *selling* company; decision-maker audience.
|
||
- **demo-script-1pager** — 7-ish numbered steps (Do + Say), prints on one A4.
|
||
|
||
### Hand off
|
||
|
||
Output: the login, the pre-loaded data cheat-sheet, the verification log, and links to the
|
||
three docs. Remind the user to rehearse once and keep the demo quotation in draft.
|
||
|
||
---
|
||
|
||
## Phase C — Follow-through
|
||
|
||
Runs after the demo happened. Needs: the built demo, the verification log, and what
|
||
actually came up in the room. Ask the user for the room notes — objections raised, who
|
||
pushed back, what they asked for that wasn't there.
|
||
|
||
Generate four documents, reusing `templates/` branding and its self-contained-HTML rule
|
||
(no external assets, single file, prints to A4).
|
||
|
||
| Document | Contents |
|
||
|---|---|
|
||
| **Objection sheet** | Every objection raised → the honest answer. Where the product already covers it, name the module and the screen. Where it doesn't, say so and give the workaround or the build estimate. |
|
||
| **Scoped quote** | Line items from the Phase 1 scope: modules, configuration, data migration, custom work, training. Days and rate per line. Flag assumptions separately from committed lines. |
|
||
| **Implementation timeline** | Phased, with the client's own dependencies on it (data extraction, sign-offs, who from their side). Weeks, not dates, unless a start date is agreed. |
|
||
| **Statement of work** | Deliverables, acceptance criteria, out-of-scope list, change-request process. The out-of-scope list is the point — write it properly. |
|
||
|
||
Rules:
|
||
- Every number traces to something. No invented day-rates, no invented licence costs — ask
|
||
the user for the rate card.
|
||
- Anything the demo *didn't* prove goes in the quote as build work, not as "included".
|
||
Cross-check against the verification log from *Test heroes*.
|
||
- The out-of-scope list is where the follow-through earns its keep. Anything discussed and
|
||
not quoted goes in it explicitly.
|
||
|
||
Show all four to the user before anything leaves the machine.
|
||
|
||
---
|
||
|
||
## Extension points
|
||
|
||
A vertical skill (construction, retail, …) wraps this one rather than restating it. It may
|
||
override exactly these, and nothing else:
|
||
|
||
| Extension point | What the vertical supplies | Applied at |
|
||
|---|---|---|
|
||
| **Module routing** | A pain → module table keyed on the client's own words | *Install modules*, and Phase A's `## Phase 1 scope` |
|
||
| **Solution precedence** | An extra tier ahead of native, e.g. `<vertical modules> → native Odoo → custom` | *Research native-vs-custom* |
|
||
| **Purge chain** | The exact model teardown order for its own models, replacing the generic chain | *Purge test residue* |
|
||
| **Demo data policy** | An override of what dataset gets loaded, or an instruction not to install a stock one | *Load demo data* |
|
||
| **Follow-through framing** | Vertical-specific objections and scope lines | Phase C |
|
||
|
||
Rules for the boundary:
|
||
- The vertical does **not** reimplement `scripts/`, `templates/`, or `demo_config.json` handling.
|
||
- It references steps by **name** (*Test heroes*, *Purge test residue*), never by number.
|
||
- Anything a vertical needs that isn't in the table above is a gap in *this* skill — fix it
|
||
here, so the next vertical gets it free.
|
||
|
||
---
|
||
|
||
## Guardrails
|
||
|
||
- **Never invent that a feature works — test it.** *Test heroes* is mandatory.
|
||
- **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;
|
||
do not settle for "cancelled and archived" and do not write leftovers off as a note.
|
||
- Never send anything client-facing without the user reading it first.
|
||
- Read `reference/odoo19-field-gotchas.md` before touching Odoo 19 — several core fields
|
||
were renamed.
|
||
- `discovery_brief.md`, `demo_config.json` and the follow-through docs hold client data and
|
||
API keys — keep them in the project directory, never in a scratchpad you'd forget.
|
||
|
||
## Files
|
||
|
||
- `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).
|
||
- `UPSTREAM.md` — fork point and divergence from Smetools/odoo-demo-architect.
|