Files
callista-odoo-demo/SKILL.md
T
glenn schrooyenandClaude Opus 5 adb6ebe25f Verify anything the client will see in a browser
The API can only prove a record is correct, not that the screen looks right, and
a Studio-modified form, a portal page or a report PDF can be wrong while every
API read passes. Adds a "Look at the screen" step: prefer Claude in Chrome (uses
the user's own Odoo session), fall back to a Playwright MCP, and where neither is
installed offer the choice once and accept screenshots rather than blocking the
build on a tool install.

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

23 KiB
Raw Blame History


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 ", "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 briefBuildFollow-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 (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. 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:

## 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
35 items. Direct client quotes where you have them. Mark anything you inferred as (inferred).

## Hero features
12. 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 dataBuild heroesTest 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 , 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.

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.

Look at the screen

The API tells you a record is correct. It cannot tell you the demo looks right — and the demo is a thing people watch. Anything the client will see must be visually inspected before hand-off, not inferred from a search_read:

  • a Studio-modified or custom form view (a field can be set, stored, and not on the screen)
  • website / eCommerce pages, the customer portal, the login and branding
  • any hero whose payoff is visual: a blocking dialog, a kanban, a progress bar, a report PDF
  • the run-of-show click-path itself, walked once end to end

Use a browser tool. Check what's available before this step, in this order:

  1. Claude in Chrome — first choice. It drives the user's own Chrome, so an existing Odoo session just works and no credentials get typed into a script. Load the claude-in-chrome skill before calling any of its tools.
  2. Playwright MCP — use if it's already configured, or when the check should be repeatable/headless. Needs a login step, so it wants a demo user, not the client's admin.
  3. Neither installed — say so and offer the choice, once: "I can verify the screens myself with Claude in Chrome (drives your browser, uses your Odoo session) or Playwright (headless, scriptable, needs a login). Install one, or send me screenshots." Then take the user's answer and get on with it. Screenshots from the user are a perfectly good fallback — do not block the build on a tool install.

Never claim a screen looks right if nobody looked. "Field is set" and "screen is correct" are different claims, and only one of them survives the demo.

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).
  • Anything the client will see gets looked at. UI, website, portal and report changes are verified in a browser (see Look at the screen), never signed off from API reads alone.
  • 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.