JSON-RPC is the recommended Odoo API from 19 on. odoo_connect.py now posts to /jsonrpc over stdlib urllib instead of xmlrpc.client — same x()/get_or_create()/ fields_named() surface, common.version() becomes version(), server errors raise OdooError with Odoo's message, 600s timeout so module installs survive. Also: state changes must go through the model's own button method, never a write on state. Writing the field skips the delivery order, the journal entries, the sequence and the validations a hero feature exists to demonstrate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
346 lines
18 KiB
Markdown
346 lines
18 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 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".
|
||
---
|
||
|
||
# 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, login, API key. 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 JSON-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` (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.
|
||
|
||
### 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"})` |
|
||
|
||
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)
|
||
— 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*).
|
||
- **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-RPC connector + `x()` helper (correct context-kwarg passing).
|
||
- `scripts/build_demo.py` — config-driven data + hero builder (template to adapt per engagement).
|
||
- `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.
|