From 2fd559f1ccb2e61eec96a9632db11e156e8a0a2e Mon Sep 17 00:00:00 2001 From: glenn Date: Mon, 21 Sep 2026 14:30:07 +0200 Subject: [PATCH] Update callista-odoo-demo skill (local edits: README, SKILL, field gotchas, templates) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DywdYrFTrWa4h1T3ZykPBX --- README.md | 7 ++- SKILL.md | 44 +++++++++----- reference/odoo19-field-gotchas.md | 18 ++++++ templates/README.md | 21 ++++++- templates/brand.css | 66 ++++++++++---------- templates/demo-script-1pager.template.html | 32 +++++----- templates/one-pager.template.html | 71 ++++++++++++---------- templates/run-of-show.template.html | 71 +++++++++++++--------- 8 files changed, 203 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index d7773eb..145d0df 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,14 @@ follow-through framing. They do not restate the pipeline. See ## Install ```bash -git clone ~/.claude/skills/callista-odoo-demo +git clone ~/dev/callista-odoo-demo +ln -s ~/dev/callista-odoo-demo ~/.claude/skills/callista-odoo-demo ``` +**Symlink it; do not clone straight into `~/.claude/skills/`.** A second working copy is a +second source of truth: you edit the skill in one, Claude loads the other, and the drift is +invisible until a build follows instructions you thought you had deleted. + Then ask Claude: *"build an Odoo demo for <client> from these notes."* **Do not** install this as `odoo-demo-architect` — skills resolve by directory name, and a diff --git a/SKILL.md b/SKILL.md index bd21c95..220b559 100644 --- a/SKILL.md +++ b/SKILL.md @@ -42,7 +42,7 @@ question whose answer is sitting in the file you just read. | **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 | +| **Odoo instance** | `scripts/creds.json` or `ODOO_URL`/`ODOO_KEY` in the environment, 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 | @@ -182,11 +182,14 @@ 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"})` | +| `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"})` | + +`ids` is already the list — `x()` wraps a bare int for you, so `[ids]` around a list sends a +nested list and the server rejects it. 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 @@ -215,7 +218,7 @@ are heavy; allow several minutes. 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 +Drive it from `scripts/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 @@ -329,8 +332,11 @@ 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: +From `templates/` (brand-themed, `{{placeholder}}`-driven), produce three single-file HTML +docs filled with the client's data. Inline `brand.css` into each; the **only** external +reference any doc may carry is the Lato webfont `@import`, which falls back to the system +stack offline (see `templates/README.md`). No other external asset, ever — these get +emailed, opened on a strange laptop, and printed in a room with no wifi. - **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 @@ -350,8 +356,8 @@ Runs after the demo happened. Needs: the built demo, the verification log, and w 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). +Generate four documents, reusing `templates/` branding and its single-file HTML rule +(inlined CSS, no external asset but the Lato webfont, prints to A4). | Document | Contents | |---|---| @@ -385,11 +391,18 @@ override exactly these, and nothing else: | **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 | +Beyond those five, a vertical may **attach step-local notes** to any named step — the +domain detail that only makes sense for its industry (which settings default off after its +modules install, what a plausible staged number looks like, which chain *Test heroes* has +to exercise). Those change no mechanism, so they need no extension point; they name the +step they apply at and stop there. + 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. +- A vertical needing new *mechanism* — a step that doesn't exist, an override the table + doesn't offer — is a gap in *this* skill. Fix it here, so the next vertical gets it free. + Domain detail hung off an existing step is not a gap; it belongs in the vertical. --- @@ -409,8 +422,9 @@ Rules for the boundary: - Never send anything client-facing without the user reading it first. - Read `reference/odoo19-field-gotchas.md` before touching Odoo 19/20 — 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. +- `discovery_brief.md`, `scripts/demo_config.json` and the follow-through docs hold client + data, and `scripts/creds.json` holds the API key — keep them in the project directory, + never in a scratchpad you'd forget. Both json files are gitignored; keep it that way. ## Files diff --git a/reference/odoo19-field-gotchas.md b/reference/odoo19-field-gotchas.md index 91a44d0..bb88ace 100644 --- a/reference/odoo19-field-gotchas.md +++ b/reference/odoo19-field-gotchas.md @@ -60,3 +60,21 @@ A custom module that sets `category_id` on `res.groups` or `groups_id` on `res.u ## 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. + +## Custom module on v19 — five that cost a build cycle each +- **`stock.move.name` is gone.** `_rec_name = 'reference'`. Passing `name` in move vals raises. Set `description_picking` if you need text on the line. +- **Search views: `` no longer validates.** RelaxNG rejects the attributes (`ERROR:RELAXNGV:RELAXNG_ERR_NOELEM`, reported only as "Invalid view definition"). Use a bare `` wrapping the group-by ``s, as core does. +- **Mixed stored/non-stored fields in one compute → `UserWarning: inconsistent 'store'`**, and the non-stored one silently recomputes the stored ones. Give non-stored fields their own compute method. +- `sale.order.line` uses **`product_uom_id`** in v19 while `stock.move` still uses **`product_uom`**. `_action_launch_stock_rule` is keyword-only: `def _action_launch_stock_rule(self, *, previous_product_uom_qty=False)`. +- Suppressing the automatic delivery on confirmation (blanket/call-off orders) is cleanest by overriding `sale.order.line._action_launch_stock_rule` to skip those lines — no picking is created at all, so nothing has to be cancelled afterwards. + +## Local Odoo 19 Enterprise in Docker (no Odoo.sh, no sudo needed) +`postgres:16` + the official `odoo:19` image (it carries every Python dep), but **run the mounted 19.0 source, not the image's copy**: +```yaml +entrypoint: ["/bin/bash","-lc","rm -rf /usr/lib/python3/dist-packages/odoo && exec python3 /mnt/odoo/odoo-bin \"$$@\"","--"] +command: ["-c","/etc/odoo/odoo.conf"] +``` +Without the `rm`, `odoo.addons` is a **namespace package spanning both trees**: core loads from the mount while half the addons load from dist-packages — a silent version mix. Verify with a traceback path or `python3 -c "import odoo"` (should fail). +- Config keys: `http_port` (not `xmlrpc_port`), `gevent_port`. `--without-demo=all` is now a boolean — pass `--without-demo=True`. +- Driving the build through `odoo-bin shell` (script on stdin) beats the external API **on a local box only**: full ORM, no named-argument footguns, and **nothing commits unless you call `env.cr.commit()`** — which makes a dry-run of the live demo beat trivially safe (`env.cr.rollback()`). It needs shell access to the container, so it is not an option on Odoo.sh or Odoo Online; there, JSON-2 remains the only route. +- **After a CLI `-u `, restart the server container.** The upgrade runs in its own process: the DB gets the new fields and views, the long-running web process keeps its old registry. The browser then loads a view referencing a field that server doesn't know → `OwlError: "model"."field" field is undefined`, reported as an Owl lifecycle crash. `odoo-bin shell` scripts never see it (fresh registry per process), so **the whole test suite can be green while the UI is broken** — verify a renamed/added field with `fields_get` over JSON-2 against the *running* server, not only through shell. diff --git a/templates/README.md b/templates/README.md index be6b3b2..89a6c69 100644 --- a/templates/README.md +++ b/templates/README.md @@ -1,8 +1,23 @@ # Doc templates -Three self-contained HTML docs the skill produces in step 8. Inline `brand.css` -into each (one ` diff --git a/templates/one-pager.template.html b/templates/one-pager.template.html index 5904ead..1b228bf 100644 --- a/templates/one-pager.template.html +++ b/templates/one-pager.template.html @@ -5,46 +5,53 @@ {{SELLER}} × {{CLIENT}} — {{HEADLINE_SHORT}} @@ -85,7 +92,7 @@ td.was{color:var(--dim)}td.now b{color:var(--navy)}
-
{{PROOF1_N}}
{{PROOF1_L}}
+
{{PROOF1_N}}
{{PROOF1_L}}
{{PROOF2_N}}
{{PROOF2_L}}
{{PROOF3_N}}
{{PROOF3_L}}
{{PROOF4_N}}
{{PROOF4_L}}
@@ -115,7 +122,7 @@ td.was{color:var(--dim)}td.now b{color:var(--navy)} here becomes the number you are held to. -->

Investment and timing

-
{{PRICE}}
Phase 1
+
{{PRICE}}
Phase 1
{{ALL_PHASES}}
All phases
{{DURATION}}
Duration
{{PAYMENT_TERMS}}
Payment terms
diff --git a/templates/run-of-show.template.html b/templates/run-of-show.template.html index 9f99d80..1f05393 100644 --- a/templates/run-of-show.template.html +++ b/templates/run-of-show.template.html @@ -5,43 +5,54 @@ {{CLIENT}} — Run of Show @@ -73,7 +84,7 @@ tr.star td{background:#fffdf6}

What is standing by

-
{{STAT1_N}}
{{STAT1_L}}
+
{{STAT1_N}}
{{STAT1_L}}
{{STAT2_N}}
{{STAT2_L}}
{{STAT3_N}}
{{STAT3_L}}
{{STAT4_N}}
{{STAT4_L}}