Reseam onto callista odoo demo #1
@@ -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.
|
||||||
@@ -1,208 +1,135 @@
|
|||||||
---
|
---
|
||||||
name: buildease-demo
|
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
|
# 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/`,
|
**Requires** `callista-odoo-demo` installed at `~/.claude/skills/callista-odoo-demo/`
|
||||||
from github.com/Smetools/odoo-demo-architect). If it is missing, say so and stop —
|
(Callista's fork of Smetools/odoo-demo-architect). If it is missing, say so and stop —
|
||||||
do not reimplement its XML-RPC connector or data builder.
|
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
|
*Applies at:* **Install modules**, and Phase A's `## Phase 1 scope`.
|
||||||
would have emitted, from whatever the user actually has.
|
|
||||||
|
|
||||||
**Source, in order of preference:**
|
Read `reference/buildease.md` and route the client's pains onto BuildEase modules using
|
||||||
1. A transcript, meeting notes, or email thread — read it.
|
its pain → module table. Match on **the client's own words** — the table is keyed on what
|
||||||
2. Knowcap MCP, if it happens to be available and the user wants it.
|
they actually say ("we measure in Excel then re-type it into the quote"), not on module
|
||||||
3. Nothing written down → interview the user (see intake below).
|
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,
|
## Overlay 2 — Solution precedence
|
||||||
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 |
|
*Applies at:* **Research native-vs-custom**.
|
||||||
|---|---|---|
|
|
||||||
| **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 |
|
|
||||||
|
|
||||||
Anything still missing after that batch: write it into the brief as `(unknown)` and
|
The precedence becomes **BuildEase → native Odoo 19 → custom**, in that order. Most
|
||||||
carry on. One unknown does not block building the demo.
|
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
|
Odoo 19 only — every mev manifest is `19.0.x`. Do not attempt this against 17 or 18.
|
||||||
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.
|
|
||||||
|
|
||||||
**Write `discovery_brief.md` with these six sections and no others:**
|
## Overlay 3 — Demo data policy
|
||||||
|
|
||||||
```markdown
|
*Applies at:* **Load demo data**.
|
||||||
## Business shape
|
|
||||||
Entities, product/customer types, volumes, shared financials.
|
|
||||||
Company name, website, country, what they actually build — from the site if you fetched it.
|
|
||||||
|
|
||||||
## Top pains
|
**Do not install `buildease_demo_data` or `buildease_demo_data_sale_progress`.** They are
|
||||||
3–5 items. Direct client quotes where you have them. Mark anything you inferred as (inferred).
|
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
|
Install them only if the user explicitly asks for the generic sandbox. The app-level
|
||||||
1–2. Each names the pain it kills and the decision-maker it lands on.
|
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
|
**Read `reference/buildease_demo_data.md` before writing the staging script.** It carries
|
||||||
Odoo apps + BuildEase modules, from reference/buildease.md.
|
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.
|
||||||
|
|
||||||
## Odoo instance
|
`scripts/build_demo.py` is a per-engagement template, not an engine — it knows nothing about
|
||||||
URL, database, login, API key. Ask if absent — do not guess.
|
take-offs, and it ignores config keys it doesn't recognise. Adapt it; do not assume a
|
||||||
UI language + `l10n_*` localisation to install.
|
BuildEase-shaped config will drive it.
|
||||||
|
|
||||||
## Demo audience
|
## Overlay 4 — Purge chain
|
||||||
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
|
*Applies at:* **Purge test residue**, replacing the generic chain.
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
transient wizard rows → statements (cancelled only, newest first) → invoices
|
transient wizard rows → statements (cancelled only, newest first) → invoices
|
||||||
→ timesheet lines → tasks → projects → sale orders → take-offs → the partner
|
→ 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 take-off with no sale order cannot be deleted at all.** Its state is derived from the
|
a debugging round:
|
||||||
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.
|
|
||||||
|
|
||||||
Finish by reading back the counts of every model you touched and putting them in the
|
- **Cancel before deleting, on all three.** `unlink()` refuses unless `quantity.take.off` is
|
||||||
verification log. "No test residue" is a claim; the counts are the evidence.
|
`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.
|
||||||
|
|
||||||
### Sanity-check the staged numbers before calling it done
|
Full field-level detail, including the guards, is in `reference/buildease_demo_data.md`.
|
||||||
|
|
||||||
A chain that passes every assertion can still look broken on screen. Check the staged data
|
## Overlay 5 — Follow-through framing
|
||||||
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
|
*Applies at:* **Phase C**.
|
||||||
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
|
- After installing, **walk Settings once**. `res.config.settings` and `res.company` are
|
||||||
actually came up in the room. Ask the user for the room notes — objections raised,
|
extended by 8 modules each and several BuildEase behaviours default to off.
|
||||||
who pushed back, what they asked for that wasn't there.
|
- 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.
|
||||||
Generate four documents. Reuse odoo-demo-architect's `templates/` branding and its
|
- The spine of every BuildEase demo is QTO → sale order → project → progress statement →
|
||||||
self-contained-HTML rule (no external assets, single file, prints to A4).
|
invoice. If that chain breaks mid-demo you lose the deal, so it is the chain **Test
|
||||||
|
heroes** must exercise end to end.
|
||||||
| Document | Contents |
|
- `quantity.take.off` inherits `mail.thread` + `mail.activity.mixin` — chatter and
|
||||||
|---|---|
|
activities work for free. Cheap demo beats.
|
||||||
| **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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Guardrails
|
## Guardrails
|
||||||
|
|
||||||
- Never claim a BuildEase feature works without having seen it work in the demo DB.
|
- 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;
|
- Never invent a pain quote. `(inferred)` is fine; a fabricated quote read back to the
|
||||||
do not settle for "cancelled and archived" and do not write the leftovers off as a note
|
client in the demo is not.
|
||||||
in the verification log.
|
- If something you need is not one of the five overlays above, that is a gap in
|
||||||
- Never send anything client-facing without the user reading it first.
|
`callista-odoo-demo` — fix it there, so the next vertical gets it free. Do not work
|
||||||
- `discovery_brief.md`, `demo_config.json` and the follow-through docs hold client data
|
around it here.
|
||||||
and API keys — keep them in the project directory, never in a scratchpad you'd forget.
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `reference/buildease.md` — the module map, the pain → module routing table, and the mev
|
||||||
|
gotchas. Read it at Phase A.
|
||||||
|
|||||||
@@ -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.**
|
**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
|
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
|
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
|
per client from the discovery brief and loaded through `callista-odoo-demo`'s
|
||||||
`build_demo.py` / `demo_config.json`.
|
`scripts/build_demo.py` / `demo_config.json`.
|
||||||
|
|
||||||
Install them only if the user explicitly asks, or when there is genuinely no client
|
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:
|
context and a generic sandbox is wanted. If you do, know what comes with them:
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user