From 96d02fe0d740f7811019711fa48f2b0edeb68d6f Mon Sep 17 00:00:00 2001 From: glenn Date: Fri, 31 Jul 2026 10:55:31 +0200 Subject: [PATCH 1/2] Reseam: reduce to a vertical overlay on callista-odoo-demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of this skill was never construction-specific. Phase A (discovery brief from any source), the two-agent parallel build, the generic purge craft and Phase C (follow-through docs) applied to any Odoo demo — but sat behind a BuildEase trigger, so Callista's plain Odoo implementations could not reach them and still ran upstream's Knowcap-bound step 1 with no follow-through. Those moved to callista-odoo-demo (Callista's fork of Smetools/odoo-demo- architect, which we cannot PR). What is left here is the part that is actually about construction, expressed as five overlays on named substrate steps: module routing, solution precedence, demo-data policy, purge chain, and follow-through framing. SKILL.md goes from 208 lines to 119. Nothing BuildEase-specific was dropped — the take-off-without-a-sale-order trap, the dynamic-article-template-line pin, the settings-default-off walk, the booked-vs-ordered-hours check and the custom_product_* dependency gap all survive. Two rules now carry the boundary: anything needed that is not one of the five overlays is a gap to fix in the substrate rather than work around here, and substrate steps are referenced by name, never by number — numbering was how an upstream renumber could silently misroute this skill. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 61 +++++++++++ SKILL.md | 243 +++++++++++++---------------------------- reference/buildease.md | 4 +- 3 files changed, 140 insertions(+), 168 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ffd9b53 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repository is + +This repo *is* a Claude Code skill, not an application. It contains no source code, tests, or build tooling — only prompt content: + +- `SKILL.md` — the skill body (`name: buildease-demo`), loaded by Claude Code when the skill is invoked. +- `reference/buildease.md` — the BuildEase module map and pain→module routing table, read at Phase A. + +Editing this repo means editing instructions that another Claude instance will follow. There is nothing to compile or lint; the only meaningful verification is running the skill end to end against a real Odoo instance. + +### Installing / testing a change + +```bash +ln -s "$PWD" ~/.claude/skills/buildease-demo +``` + +Claude Code discovers skills from the `name:`/`description:` frontmatter in `SKILL.md`. Changing `name:` changes how the skill is invoked; changing `description:` changes when it triggers — both are the routing surface, so treat them as API. + +## Architecture + +`buildease-demo` is a **thin vertical overlay** on `callista-odoo-demo`. It does not own a pipeline. It supplies exactly five overlays, each applied at a *named* step of the substrate skill: + +| Overlay | Applied at | +|---|---| +| Module routing (`reference/buildease.md`) | *Install modules*, Phase A `## Phase 1 scope` | +| Solution precedence — BuildEase → native Odoo 19 → custom | *Research native-vs-custom* | +| Demo data policy — never `buildease_demo_data` | *Load demo data* | +| Purge chain — take-off / progress-statement teardown order | *Purge test residue* | +| Follow-through framing | Phase C | + +**The boundary is the point of this repo.** Anything needed that isn't one of those five is a gap in `callista-odoo-demo` — fix it *there*, so the next vertical inherits it. Do not work around it here, and never reimplement the substrate's `scripts/`, `templates/`, or `demo_config.json` handling. + +This repo was previously a full three-phase skill. The generic two-thirds (discovery brief from any source, the two-agent parallel build, the follow-through doc set) moved into the substrate on 2026-07-31 because Callista runs it for plain Odoo implementations too, and that content was stranded behind a construction trigger. + +### Substrate and upstream + +| Repo | Role | +|---|---| +| `admin/callista-odoo-demo` (Gitea) | The pipeline this skill overlays. Hard dependency, installed as `~/.claude/skills/callista-odoo-demo/` | +| `github.com/Smetools/odoo-demo-architect` | What the substrate was forked from at `ada2751`. Callista has no commit rights; changes land in the fork. See its `UPSTREAM.md` | +| `git@github.com:callista-tools/mev.git` | BuildEase — 34 Odoo 19 modules, umbrella module `buildease`. Not vendored; `reference/buildease.md` is a distilled map | + +**Never install the substrate under the name `odoo-demo-architect`.** Skills resolve by directory name, so a same-named fork silently shadows upstream depending on install order, and two people build different demos from the same brief. + +### Editing rules that carry weight + +- **Reference substrate steps by name** (*Test heroes*, *Purge test residue*), never by number. Numbered references are how an upstream renumbering silently misroutes this skill with nothing to detect it. +- **The purge ordering** (transient wizard rows → statements → invoices → timesheets → tasks → projects → sale orders → take-offs → partner last) is ordered because Odoo blocks deletion otherwise. Do not reorder or summarise. The take-off-without-a-sale-order trap in particular is non-obvious and cost a debugging round. +- **Not installing `buildease_demo_data`** is the reason this overlay exists at all; the app-level dependencies that bundle used to drag in must still be routed from the pain table. +- `reference/buildease.md` drifts when mev changes. When updating it, keep the two things that make it useful: the pain→module table keyed on *the client's own words*, and the gotchas (missing `custom_product_*` deps, Odoo 19 only, settings that default off). + +## Writing style for this repo + +Match the existing prose: imperative, second person, no hedging, tables for anything enumerable. Rules state what to do *and* the failure that motivates them ("a fabricated quote read back to the client in the demo is not"). Avoid generic agent boilerplate — everything here should be specific to BuildEase or it belongs in the substrate. + +## Related + +The parent workspace `../CLAUDE.md` (Callista BV multi-customer workspace) covers Odoo module conventions — naming, manifests, version numbering — and applies when editing the mev modules themselves, not this repo. diff --git a/SKILL.md b/SKILL.md index 43f1471..113ada8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,208 +1,119 @@ --- name: buildease-demo -description: Wraps odoo-demo-architect for BuildEase (callista-tools/mev) construction demos. Builds the discovery brief from any source — transcript file, notes, email thread, or a live interview — instead of requiring Knowcap, routes client pains onto BuildEase modules before considering custom work, and after the demo generates the follow-through set (objection sheet, scoped quote, implementation timeline, SOW). Use for "BuildEase demo", "QTO demo", "construction Odoo demo", "prep a demo from these notes", or "write the follow-up after the demo". +description: BuildEase (callista-tools/mev) construction overlay for the callista-odoo-demo skill. Routes the client's pains onto the 34 BuildEase Odoo 19 modules before any custom work is considered, replaces the generic teardown with the take-off → progress-statement chain, and keeps the per-client dataset instead of the old buildease_demo_data bundle. Use for "BuildEase demo", "QTO demo", "quantity take-off demo", "construction Odoo demo", "vorderingsstaat demo", or a follow-up after one. --- # BuildEase Demo -Three phases. `odoo-demo-architect` does the middle one; this skill owns the ends. +A **vertical overlay** on `callista-odoo-demo`. That skill owns the pipeline — discovery +brief, build, test, purge, sales docs, follow-through. This one supplies the five things +that are specific to construction, and nothing else. -**Requires** `odoo-demo-architect` installed (`~/.claude/skills/odoo-demo-architect/`, -from github.com/Smetools/odoo-demo-architect). If it is missing, say so and stop — -do not reimplement its XML-RPC connector or data builder. +**Requires** `callista-odoo-demo` installed at `~/.claude/skills/callista-odoo-demo/` +(Callista's fork of Smetools/odoo-demo-architect). If it is missing, say so and stop — +do not reimplement its XML-RPC connector, data builder, or pipeline here. + +> Do **not** substitute an `odoo-demo-architect` install for it. That is upstream, without +> Phase A's source-agnostic discovery, without Phase C, and without the extension points +> this file plugs into. + +**Run `callista-odoo-demo` and apply the overlays below at the named steps.** Everything +not listed here is that skill's job — follow it as written. --- -## Phase A — Discovery brief (no Knowcap) +## Overlay 1 — Module routing -Goal: produce `discovery_brief.md` in exactly the shape odoo-demo-architect's step 1 -would have emitted, from whatever the user actually has. +*Applies at:* **Install modules**, and Phase A's `## Phase 1 scope`. -**Source, in order of preference:** -1. A transcript, meeting notes, or email thread — read it. -2. Knowcap MCP, if it happens to be available and the user wants it. -3. Nothing written down → interview the user (see intake below). +Read `reference/buildease.md` and route the client's pains onto BuildEase modules using +its pain → module table. Match on **the client's own words** — the table is keyed on what +they actually say ("we measure in Excel then re-type it into the quote"), not on module +names they will never utter. -### Intake — infer first, ask only the gaps +Install the umbrella module `buildease` plus the routed set. `buildease` depends on +nothing and installs the set. -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. +## Overlay 2 — Solution precedence -| 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, 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 | +*Applies at:* **Research native-vs-custom**. -Anything still missing after that batch: write it into the brief as `(unknown)` and -carry on. One unknown does not block building the demo. +The precedence becomes **BuildEase → native Odoo 19 → custom**, in that order. Most +construction pains are already solved in mev; custom work is the last resort, and native +Odoo is the middle tier, not the first question. -If nothing written down exists at all, the interview replaces the table — still one -batch: who the client is, what they build, how they quote today, how they invoice, -the top 3 complaints they voiced, who is in the room. +Odoo 19 only — every mev manifest is `19.0.x`. Do not attempt this against 17 or 18. -**Write `discovery_brief.md` with these six sections and no others:** +## Overlay 3 — Demo data policy -```markdown -## Business shape -Entities, product/customer types, volumes, shared financials. -Company name, website, country, what they actually build — from the site if you fetched it. +*Applies at:* **Load demo data**. -## Top pains -3–5 items. Direct client quotes where you have them. Mark anything you inferred as (inferred). +**Do not install `buildease_demo_data` or `buildease_demo_data_sale_progress`.** They are +the previous generation of environment setup: one fixed Belgian construction dataset, the +same for every prospect, dragging in `l10n_be` and `accountant` whether or not the client +is Belgian. The per-client dataset generated from the discovery brief replaces them — the +client's own partners, articles, projects and orders, staged so each hero has a record to +fire on. -## Hero features -1–2. Each names the pain it kills and the decision-maker it lands on. +Install them only if the user explicitly asks for the generic sandbox. The app-level +dependencies that bundle used to drag in (`sale_management`, `project`, `purchase_stock`, +`sale_timesheet`, `industry_fsm`, `accountant`, `uom`) still need installing — route them +from the pain table, not from the demo bundle. -## Phase 1 scope -Odoo apps + BuildEase modules, from reference/buildease.md. +## Overlay 4 — Purge chain -## 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, via odoo-demo-architect - -Hand `discovery_brief.md` to `odoo-demo-architect` and let it run its pipeline -(research → connect → install → data → heroes → test → sales docs). - -### Fork the slow half - -Module installation and client research are independent — the install list comes from the -brief plus the routing table, the research feeds hero *building* which happens after both. -Installing over XML-RPC is minutes of waiting and produces log spew nobody needs in context. -So run them as two parallel subagents, launched in **one** message: - -**Agent 1 — instance prep.** Give it the Odoo credentials, the routed module list, and -odoo-demo-architect's `scripts/odoo_connect.py`. It sets the company, currency, UoM and -UI language, installs the localisation, installs `buildease` + the routed modules, then -walks Settings for the BuildEase toggles 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 **BuildEase → native Odoo 19 → custom**, in that order. -Returns: per hero, which of the three it lands in, and the click-path if it already exists. - -**Join.** Both done → install any module Agent 2's research turned up that Agent 1 didn't -know about (incremental, cheap), then generate and load the client-specific dataset into -the empty database, then build and test heroes. - -Two agents, not five. Do not fan out further — hero building is sequential against one -database and parallel writes will fight. - -Two things to inject before it starts: - -1. **Read `reference/buildease.md`** and pass the routed module list. The research step's - native-vs-custom question becomes **BuildEase → native Odoo → custom**, in that order. - Most construction pains are already solved in mev; custom work is the last resort. -2. **This skill is the demo data loader.** Do not install `buildease_demo_data` — that is - the old, one-size-fits-all Belgian dataset it replaces. The database starts empty and - gets a dataset generated from the brief: the client's own partners, articles, projects - and orders, staged so each hero feature has a record to fire on. Install it only if the - user explicitly asks for the generic sandbox. - -Its step 7 (test every hero on throwaway records) is not optional here either. A QTO → -sale order → project → progress statement → invoice chain that breaks mid-demo is the -one failure mode that loses the deal. - -### Verify on throwaway records, then delete them - -Test against a partner named `ZZ Test…`, never against 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. - -**Then purge. A demo environment ships with zero test residue.** Debugging the chain takes -several runs, each leaving a full take-off → order → project → statement → invoice 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 of this unless you go in order: +*Applies at:* **Purge test residue**, replacing the generic chain. ``` transient wizard rows → statements (cancelled only, newest first) → invoices → timesheet lines → tasks → projects → sale orders → take-offs → the partner ``` -The traps, all of which cost a debugging round: +The generic traps still apply. These are the BuildEase-specific ones, each of which cost +a debugging round: + - **A take-off with no sale order cannot be deleted at all.** Its state is derived from the order, so there is nothing to cancel. Convert it to a quotation, cancel that, then delete. -- `search` will not find the partner once archived — pass `context={"active_test": False}` - or the purge silently no-ops and reports success. -- Statements cancel newest-first only; invoices must go to draft before cancel; leftover - `add.dynamic.article.template.line` rows pin take-offs. -- **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. +- Leftover `add.dynamic.article.template.line` rows pin take-offs. Clear them first. +- Statements cancel newest-first only. -Finish by reading back the counts of every model you touched and putting them in the -verification log. "No test residue" is a claim; the counts are the evidence. +## Overlay 5 — Follow-through framing -### Sanity-check the staged numbers before calling it done +*Applies at:* **Phase C**. -A chain that passes every assertion can still look broken on screen. Check the staged data -reads plausibly: **booked hours well under ordered hours** (over 100% makes the progress -statement 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. +Where an objection is already covered by BuildEase, name the **module and the screen** on +the objection sheet — "that's `sale_progress_revision_formula`, on the statement's Revision +tab" lands better than "yes, we support that". Where mev does not cover it, say so and give +the workaround or the build estimate. + +Check the scoped quote against `reference/buildease.md`'s gotchas before pricing: +`custom_product_sale`, `custom_product_bom` and `bom_on_sale_order_line` are **not in the +mev repo**, so anything under `buildease_custom_product*` is not a "just install it" line. --- -## Phase C — Follow-through +## BuildEase-specific checks -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. Reuse odoo-demo-architect's `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 BuildEase 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 step 7. -- 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. - ---- +- After installing, **walk Settings once**. `res.config.settings` and `res.company` are + extended by 8 modules each and several BuildEase behaviours default to off. +- At the staged-numbers sanity check: **booked hours well under ordered hours**. Over 100% + makes the progress statement look wrong on screen even when every assertion passed. +- The spine of every BuildEase demo is QTO → sale order → project → progress statement → + invoice. If that chain breaks mid-demo you lose the deal, so it is the chain **Test + heroes** must exercise end to end. +- `quantity.take.off` inherits `mail.thread` + `mail.activity.mixin` — chatter and + activities work for free. Cheap demo beats. ## Guardrails - Never claim a BuildEase feature works without having seen it work in the demo DB. -- 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 the leftovers off as a note - in the verification log. -- Never send anything client-facing without the user reading it first. -- `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. +- Never invent a pain quote. `(inferred)` is fine; a fabricated quote read back to the + client in the demo is not. +- If something you need is not one of the five overlays above, that is a gap in + `callista-odoo-demo` — fix it there, so the next vertical gets it free. Do not work + around it here. + +## Files + +- `reference/buildease.md` — the module map, the pain → module routing table, and the mev + gotchas. Read it at Phase A. diff --git a/reference/buildease.md b/reference/buildease.md index c6a298e..0d62cda 100644 --- a/reference/buildease.md +++ b/reference/buildease.md @@ -48,8 +48,8 @@ Match the client's own words on the left; install the right. **Do not install `buildease_demo_data` or `buildease_demo_data_sale_progress` by default.** They are the previous generation of environment setup: one fixed Belgian construction dataset, the same for every prospect. This skill replaces them — demo data is generated -per client from the discovery brief and loaded through odoo-demo-architect's -`build_demo.py` / `demo_config.json`. +per client from the discovery brief and loaded through `callista-odoo-demo`'s +`scripts/build_demo.py` / `demo_config.json`. Install them only if the user explicitly asks, or when there is genuinely no client context and a generic sandbox is wanted. If you do, know what comes with them: -- 2.54.0 From 17ebc96bdc1670fda578d4f828ef8408f36a9814 Mon Sep 17 00:00:00 2001 From: glenn Date: Fri, 31 Jul 2026 11:10:29 +0200 Subject: [PATCH 2/2] Add the BuildEase demo-data reference, read from mev source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overlay 3 said what not to install and gave no shape for what to build instead, so the staging script was being written from scratch each time against models nobody had the field list for. reference/buildease_demo_data.md carries the QTO -> sale order -> project -> progress statement -> invoice graph: required fields per model, creation order, which transitions are button methods rather than writes, and the seven guards that stop the chain mid-build (missing product blocks conversion; the SO will not confirm while the QTO is in QTO state; no statement until the QTO is Confirmed; statement periods may not overlap; a QTO-linked order cannot be copied at all). All of it read from callista-tools/mev @ 43c2e19 and Odoo 19 core, nothing inferred. The config block is labelled illustrative because build_demo.py cannot execute it — it is a per-engagement template that silently ignores keys it does not implement, which is now an error upstream in callista-odoo-demo. Two purge corrections fall out of the source, both loosening rules that were stricter than reality: - Deleting the take-off already deletes its sale order; unlink() cascades. - A take-off with no sale order cancels and deletes normally. cancel() writes the state directly and no-ops on the empty order, since Odoo 19's action_cancel has no ensure_one(). The convert-to-quotation-first workaround was unnecessary. Co-Authored-By: Claude Opus 5 --- SKILL.md | 22 ++- reference/buildease_demo_data.md | 226 +++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 reference/buildease_demo_data.md diff --git a/SKILL.md b/SKILL.md index 113ada8..23d0b5d 100644 --- a/SKILL.md +++ b/SKILL.md @@ -60,6 +60,15 @@ dependencies that bundle used to drag in (`sale_management`, `project`, `purchas `sale_timesheet`, `industry_fsm`, `accountant`, `uom`) still need installing — route them from the pain table, not from the demo bundle. +**Read `reference/buildease_demo_data.md` before writing the staging script.** It carries +the QTO → sale order → project → progress statement → invoice object graph from mev source: +required fields per model, creation order, which transitions are button methods rather than +writes, and the seven guards that will otherwise stop the chain mid-build. + +`scripts/build_demo.py` is a per-engagement template, not an engine — it knows nothing about +take-offs, and it ignores config keys it doesn't recognise. Adapt it; do not assume a +BuildEase-shaped config will drive it. + ## Overlay 4 — Purge chain *Applies at:* **Purge test residue**, replacing the generic chain. @@ -72,10 +81,17 @@ transient wizard rows → statements (cancelled only, newest first) → invoices The generic traps still apply. These are the BuildEase-specific ones, each of which cost a debugging round: -- **A take-off with no sale order cannot be deleted at all.** Its state is derived from the - order, so there is nothing to cancel. Convert it to a quotation, cancel that, then delete. +- **Cancel before deleting, on all three.** `unlink()` refuses unless `quantity.take.off` is + `cancel`, a QTO-linked `sale.order` is `cancel`, and `sale.progress` is `cancelled`. +- **Deleting the take-off deletes its sale order** — `quantity.take.off.unlink()` unlinks + `sale_order_ids` first. The take-off is the handle for both. +- A take-off with **no** sale order still cancels and deletes normally: call `cancel()`, which + writes the state directly and no-ops on the empty order. No convert-to-quotation dance. +- **Statements cancel newest-first only** — `action_cancel` refuses unless it is the last one, + and `action_draft` refuses if a newer statement exists. - Leftover `add.dynamic.article.template.line` rows pin take-offs. Clear them first. -- Statements cancel newest-first only. + +Full field-level detail, including the guards, is in `reference/buildease_demo_data.md`. ## Overlay 5 — Follow-through framing diff --git a/reference/buildease_demo_data.md b/reference/buildease_demo_data.md new file mode 100644 index 0000000..3b97a42 --- /dev/null +++ b/reference/buildease_demo_data.md @@ -0,0 +1,226 @@ +# BuildEase demo data — object graph, required fields, creation order + +Everything here is read from mev source, not inferred. Verified against +`callista-tools/mev @ 43c2e19` (`quantity_take_off_base` 19.0.1.2.2) and Odoo 19 core. +Re-verify after a mev bump; the guards below are enforced in Python, so they change. + +## Read this first: there is no BuildEase config schema + +`callista-odoo-demo`'s `scripts/build_demo.py` is **a per-engagement template, not an +engine** — its own docstring says so. It has hard-coded blocks for products, fleet +vehicles, credit limits and prestaged receivables, and no concept of a take-off. It also +reads every key with `cfg.get(...)`, so **any key it doesn't know is silently ignored**. + +Feed it a config full of `take_offs` and it prints `DONE` over an empty database. + +So: write the staging script per engagement, using this document for the shape. The config +block at the bottom is **illustrative** — a description of what a BuildEase dataset +contains, not something the stock builder executes. + +--- + +## The spine + +``` +partner + products → quantity.take.off → .line → .line.calculation + → convert_to_quotation() → sale.order (draft) → action_confirm() → sale (project) + → sale.progress (+ lines) → approve → invoice +``` + +Build in that order. Each step has a guard that blocks the next if skipped. + +| # | Create | Then | +|---|---|---| +| 1 | `res.partner` (customer), `product.product` with `sale_ok=True` | — | +| 2 | `quantity.take.off` | lines | +| 3 | `quantity.take.off.line` (`type="line"`) | calculation lines | +| 4 | `quantity.take.off.line.calculation` | trigger `_compute_unit_price` | +| 5 | `convert_to_quotation()` on the QTO | SO exists in `draft`, QTO moves to `draft` | +| 6 | `action_confirm()` on the SO | SO `sale`; QTO mirrors to `sale` | +| 7 | `sale.progress` via the wizard | progress lines auto-built from SO lines | +| 8 | Set `current_quantity` / `percentage` on progress lines | approve, then invoice | + +--- + +## `quantity.take.off` + +| Field | Note | +|---|---| +| `partner_id` | **Required.** | +| `company_id` | **Required**, defaults to `env.company`. | +| `name` | Required *and* readonly — `create()` overwrites it unconditionally from the `quantity.take.off.number` sequence. **Never pass it**; whatever you send is discarded. | +| `date` | Defaults to today. | +| `state` | Only exists once `quantity_take_off_sale` is installed: `quantity_take_off` → `draft` → `sent` → `sale` → `cancel`. Stored and writable, default `quantity_take_off`. | +| `site_address_id`, `pricelist_id`, `fiscal_position_id` | Computed from `partner_id` with `store=True, readonly=False` — they self-populate on create; only pass them to override. | +| `amount_subtotal`, `amount_total`, `margin`, `margin_percentage` | Computed from lines. Never write them. | + +Inherits `mail.thread` + `mail.activity.mixin` — chatter and activities work for free. + +**One sale order per take-off**, enforced by `@api.constrains`. A second one raises. + +## `quantity.take.off.line` + +| Field | Note | +|---|---| +| `quantity_take_off_id` | **Required**, `ondelete="cascade"`. | +| `type` | **Required.** `title` / `text` / `line` / `subtitle`, default `line`. | +| `name` | **Required** (Text, computed with `readonly=False` — pass it or give a product to compute from). | +| `quantity`, `unit_price`, `cost` | `unit_price` and `cost` are `readonly=False`, so writable directly. | +| `sequence` | Default 10. | +| `product_id` | Required when `is_product_id_required` — and missing products block conversion, see below. | + +**Only `type="line"` rows count toward the totals.** `title`, `text` and `subtitle` are +presentation. A demo whose totals read zero is usually a take-off built entirely from the +wrong `type`. + +**Pro memorie lines are excluded from every total.** If `nature_of_agreement_id.pro_memorie` +is set, the line is filtered out of `amount_subtotal`, `amount_total`, `margin` *and* out of +`convert_to_quotation`. Deliberate for "for information" lines; a silent zero if unintended. + +## `quantity.take.off.line.calculation` + +| Field | Note | +|---|---| +| `product_id` | **Required**, domain `sale_ok = True`. A non-saleable product simply won't be selectable. | +| `name` | **Required.** | +| `quantity_take_off_line_id` | **Required**, `ondelete="cascade"`. | +| `quantity` | Default 1.0. | +| `markup`, `cost`, `unit_price` | `cost` is computed-store-precompute but `readonly=False`. | + +This is the measurement math beneath a line. After creating calculation lines +programmatically, call `_compute_unit_price()` on them — `add_quantity_take_off_lines()` +does exactly that after building from templates, and prices stay stale otherwise. + +## `sale.progress` + +| Field | Note | +|---|---| +| `sale_order_id` | **Required**, readonly. | +| `start_date`, `end_date` | **Required.** Constrained: end may not precede start. | +| `state` | **Required**, readonly, default `draft`: `draft` / `sent` / `denied` / `approved` / `cancelled` / `invoiced`. | +| `name` | Auto — `sale_progress_prefix` + zero-padded count of non-cancelled statements + 1, both off `res.company`. | +| `project_id`, `responsible_user_id` | Populated from the sale order by `prepare_sale_progress_vals()`. | + +**Create it through the flow, not by hand.** `sale.order.prepare_sale_progress_vals()` +returns the full vals dict *including* every progress line built from the order lines; the +`sale.progress.create` wizard (fields `start_date`, `end_date`, `sale_order_id`) adds the +dates and calls create. Building a `sale.progress` directly means building +`sale_progress_line_ids` yourself for every order line — there is no auto-fill on create. + +## `sale.progress.line` + +`sale_progress_id` is the only required field (`ondelete="cascade"`); nearly everything else +is `related` to `sale_order_line_id`. The demo-relevant writable fields are `percentage`, +`current_quantity` and `previous_quantity` — that is where "we invoice 40% this month" lives. + +--- + +## The guards that will stop you + +In the order you'll hit them. All raise `ValidationError` or `UserError`, all are in Python. + +| Guard | Raised by | Fix | +|---|---|---| +| Missing product on a `line`/`variant` row that isn't pro memorie | `convert_to_quotation` → `_check_alerts` | Give every non-pro-memorie line a `product_id` before converting | +| "Convert To Quotation" unavailable | Button is `invisible="state != 'quantity_take_off' or sale_order_id"` | Only converts once, from the initial state | +| "You cannot confirm an order if the quantity take off is in the QTO state" | `sale.order.action_confirm` | Convert first; confirming is what moves both to `sale` | +| "You can only create a sale progress when the quantity take-off is in the 'Confirmed' state" | `sale.order.action_create_sale_progress` | Confirm the SO before staging any statement | +| "You can only create a new sale progress when all the other sale progresses are in the following states…" | same | Approve or invoice the previous statement first | +| "Choose a start date that is later than or equal to the end date of a previous sale progress" | `sale.progress.create` wizard | Stage consecutive, non-overlapping periods | +| "You cannot duplicate an order coming from a quantity take off!" | `sale.order.copy` | Duplicate the take-off instead — the SO cannot be copied at all | + +**State mirrors automatically.** `sale.order.write` propagates `draft` / `sent` / `sale` / +`cancel` onto the linked take-off. You do not set QTO state by hand once a sale order +exists, and doing so will disagree with the order. + +--- + +## Teardown + +Two corrections to the generic chain, both from source: + +**Deleting the take-off deletes its sale order.** `quantity.take.off.unlink()` calls +`self.sale_order_ids.unlink()` first. So the take-off is the handle for both; you do not +need to delete the order separately, only to have cancelled it. + +**A take-off with no sale order *can* be cancelled and deleted.** `cancel()` writes +`state = "cancel"` directly and then calls `action_cancel()` on `sale_order_id` — an empty +recordset when there is no order, and in Odoo 19 `sale.order.action_cancel` is +`self.write({'state': 'cancel'})` with no `ensure_one()`, so it is a clean no-op. Over +XML-RPC: + +```python +x("quantity.take.off", "cancel", [qto_id]) # works with or without a sale order +x("quantity.take.off", "unlink", [qto_id]) # now passes the state == 'cancel' check +``` + +This supersedes the older "convert it to a quotation first, then cancel that" workaround, +which is unnecessary on Odoo 19. + +Both `unlink()` overrides are strict about state: + +| Model | Refuses unless | +|---|---| +| `quantity.take.off` | `state == "cancel"` | +| `sale.order` (QTO-linked) | `state == "cancel"` | +| `sale.progress` | `state == "cancelled"` | + +And `sale.progress.action_cancel()` refuses unless `show_cancel` — *"You can only cancel the +last sale progress"* — while `action_draft()` refuses if a more recently created statement +exists. That is why statements tear down **newest first**, and why it is not negotiable. + +--- + +## Illustrative dataset shape + +**Not a schema.** Nothing reads this file. It records what a BuildEase demo dataset +contains so the per-engagement staging script covers it: + +```jsonc +{ + "client": "Vandenberghe Bouw", + "currency": "EUR", + "scope_modules": ["sale_management", "project", "purchase_stock", "sale_timesheet", + "uom", "accountant", "buildease", "quantity_take_off_base", + "quantity_take_off_sale", "sale_progress_base"], + + "customers": [{ "name": "Gemeente Deinze", "site_address": "Markt 21, 9800 Deinze" }], + + "articles": [ + { "article_nr": "21.32", "name": "Betonvloer gepolierd", "uom": "m²", + "cost": 42.00, "price": 68.50, "sale_ok": true }, + { "article_nr": "31.11", "name": "Metselwerk snelbouwsteen", "uom": "m²", + "cost": 55.00, "price": 89.00, "sale_ok": true } + ], + + "take_off": { + "customer": "Gemeente Deinze", + "lines": [ + { "type": "title", "name": "Ruwbouw" }, + { "type": "line", "article_nr": "21.32", "quantity": 340, + "calculations": [ { "product": "Betonvloer gepolierd", "name": "Zone A", + "quantity": 340, "markup": 12.0 } ] }, + { "type": "line", "article_nr": "31.11", "quantity": 118 } + ] + }, + + "_comment_progress": "Stage TWO consecutive statements: the first approved+invoiced so the demo has history, the second left in draft for the live click. Periods must not overlap.", + "progress_statements": [ + { "start_date": "2026-05-01", "end_date": "2026-05-31", + "lines_percentage": 35, "final_state": "invoiced" }, + { "start_date": "2026-06-01", "end_date": "2026-06-30", + "lines_percentage": 60, "final_state": "draft" } + ] +} +``` + +Staging rules that matter on screen, beyond what the guards enforce: + +- **Two statements, not one.** A single draft statement has no `previous_quantity` to show, + and the cumulative column — the reason clients ask for *vorderingsstaten* — reads as zero. +- **Leave the last statement in `draft`** for the live click, the same rule as the demo + quotation. +- **Keep cumulative percentage under 100.** Over-100 makes the statement look broken even + though every guard passed. +- **Derive quantities from the ordered quantity**, never hard-code, so they can't drift when + a markup or formula changes. -- 2.54.0