diff --git a/SKILL.md b/SKILL.md index 23d0b5d..b4bb433 100644 --- a/SKILL.md +++ b/SKILL.md @@ -31,8 +31,15 @@ its pain → module table. Match on **the client's own words** — the table is they actually say ("we measure in Excel then re-type it into the quote"), not on module names they will never utter. -Install the umbrella module `buildease` plus the routed set. `buildease` depends on -nothing and installs the set. +Install the routed set **explicitly**, module by module. `buildease` is a marker whose +manifest depends on `base` alone — it installs none of the other 33, so installing it and +stopping there leaves an empty database and no error to catch it. Install it alongside the +routed set if you want the marker; never instead of it. + +Verify afterwards: read back `ir.module.module` state for every routed name and fail loudly +on anything not `installed`. `button_immediate_install` also dies on a transient +`ir_cron` row lock (`LockNotAvailable`) when a cron is mid-run — back off and retry, it is +not a real failure. ## Overlay 2 — Solution precedence @@ -74,15 +81,25 @@ BuildEase-shaped config will drive it. *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 +transient wizard rows → invoices (draft → cancel → unlink) +→ statements (cancelled only, newest first) → timesheet lines → tasks +→ projects → sale orders → take-offs → the partner ``` The generic traps still apply. These are the BuildEase-specific ones, each of which cost a debugging round: +- **Invoices go before statements.** An `invoiced` `sale.progress` refuses `action_cancel` + outright; cancelling its invoice is what reverts it to `approved`, the first state it can + be cancelled from. Statements-then-invoices deadlocks on the one statement you invoiced. - **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`. +- **Guard each step separately and loop until a pass keeps nothing.** One `try` around + draft → cancel → unlink lets a raise on an already-draft invoice skip the unlink beneath + it, and the purge then reports success over residue. +- **Button methods return an unmarshallable action dict** over XML-RPC (`Fault` mentioning + `Marshal`/`dumps`). The transition already committed — catch it and move on. Retrying + fails on a state guard and reads like a real error. - **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 diff --git a/reference/buildease.md b/reference/buildease.md index 0d62cda..b8e4a19 100644 --- a/reference/buildease.md +++ b/reference/buildease.md @@ -1,7 +1,12 @@ # BuildEase (callista-tools/mev) — module map & pain routing Odoo **19.0** vertical for construction / contracting. Repo: `git@github.com:callista-tools/mev.git`. -34 modules, all LGPL-3. Umbrella module `buildease` depends on nothing and installs the set. +34 modules, all LGPL-3. + +**`buildease` is a marker, not an installer.** Its manifest depends on `base` and nothing +else, so installing it pulls in none of the other 33. Verified on a live 19.0+e instance: +`buildease` on its own leaves the database empty and every routed feature missing, with no +error to catch it. Always install the routed set explicitly. ## The four core concepts @@ -66,6 +71,11 @@ installing on their own — route them from the pain table above, not from the d - **Three dependencies are not in this repo**: `custom_product_sale`, `custom_product_bom`, `bom_on_sale_order_line`. Anything under `buildease_custom_product*` fails to install without them. Confirm their source before promising a BOM-pricing hero feature. + **Not in the repo does not mean not on the instance.** On the Callista demo servers all + three are present and installable — they ship with the `demo19-custom` addons path, not + with mev. Check the target instance (`ir.module.module` by name) before pricing + `buildease_custom_product*` as build work; on a server that already has them it is a + install line, and on a client's own Odoo.sh it is not. - **Odoo 19 only.** Manifests are all `19.0.x`; do not attempt against 17/18. - `l10n_be` and `accountant` are pulled in by demo data — the demo DB is Belgian-localised. Wrong-country client → set `demo_config.json` company/fiscal position explicitly, or skip diff --git a/reference/buildease_demo_data.md b/reference/buildease_demo_data.md index 3b97a42..3ee35d2 100644 --- a/reference/buildease_demo_data.md +++ b/reference/buildease_demo_data.md @@ -113,6 +113,59 @@ dates and calls create. Building a `sale.progress` directly means building 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. +**`percentage` is a FRACTION, not 0–100.** `0.60` means 60%. Writing `60` does not raise — +it sets `current_quantity` to `expected_quantity × 60`, so a 340 m² post silently becomes +20 400 m² and the statement reads in the millions. It is also **cumulative**: on the second +statement it arrives pre-filled with the previous cumulative, and writing `0.85` bills the +delta (85 m², not 289). + +--- + +## The revision formula — where the numbers actually live + +`sale_progress_revision_formula` splits the contractual formula across four models, and +none of them holds a coefficient: + +| Model | Holds | +|---|---| +| `revision.scheme` | A named set of formulas | +| `revision.formula` | `name` is a **label only**. `formula_html` is generated from the parameter count | +| `revision.parameter` | One row per index pair: `initial_index_parameter` (`S`), `current_index_parameter` (`s`) | +| `index.value.date` | The monthly series per parameter: `start_date` + `index_value` | +| **`formula.input`** | **The numbers**: `weight_value` (a, b), `initial_index_value` (S, I), `current_index_value` (s, i) | + +`formula_html` renders symbolically from the parameters — two parameters produce +`p = P × (a × s/S + b × i/I + c)` whatever you called the formula. `c` is implicit, the +remainder of the weights. + +`formula.input` hangs off **either** a take-off **or** a progress statement, and that split +is the whole feature: the take-off carries the contract reference (S, I at `start_date`), +and each statement gets its own copy carrying **that period's** index. This is what answers +*"ik moet de index van díe maand nemen, niet van vandaag"*. + +Three traps, each of which cost a debugging round: + +- **The rows are generated by an `@api.onchange`.** The list is `create="0" delete="0"` in + the form, so there is no button and no create path — and `write()` over XML-RPC does not + fire onchanges, so nothing appears. Every field on `formula.input` is a plain **stored** + field (readonly in the UI only), so write them directly, `weight_parameter` included. +- **Set `is_revision = True` on the take-off first.** `revision_scheme_id` is + `invisible="not is_revision"`, and `revision_formula_ids` computes off the *inputs*, not + off the lines — it reads empty until the inputs exist, whatever the lines say. +- **`index_value` stores to 3 decimals.** A ratio-style series near `1.0xxx` silently loses + a digit (`1.0221` → `1.022`). Use the absolute published index numbers (a wage cost of + `43.00`, a material index of `125.42`), which is what Statbel publishes anyway. + +And the failure mode with no guard behind it: a statement created **before** the contract +reference was corrected keeps the stale `initial_index_value`. The revision then divides by +the wrong denominator and computes a plausible-looking but wildly wrong number — in one +build, a factor of 65 instead of 1.005. Write **both** sides of every `formula.input`. + +The button chain is `action_price_revision` (draft → `revision`) → +`action_calculate_price_revision` (→ `revision_done`), then approve and invoice. The +statement's state selection carries `revision` and `revision_done` on top of the six states +listed above. + --- ## The guards that will stop you @@ -137,6 +190,17 @@ exists, and doing so will disagree with the order. ## Teardown +**Invoices come before statements, not after.** An `invoiced` `sale.progress` refuses +`action_cancel` outright — the guard allows only `draft` / `sent` / `denied` / `approved`, +and `invoiced` is none of them. Cancelling its **invoice** is what reverts the statement to +`approved`, which is the first state it can be cancelled from at all. So the order is: + +``` +transient wizard rows → invoices (draft → cancel → unlink) +→ statements (cancel newest-first, then unlink) → timesheet lines → tasks +→ projects → sale orders → take-offs → the partner +``` + Two corrections to the generic chain, both from source: **Deleting the take-off deletes its sale order.** `quantity.take.off.unlink()` calls @@ -169,6 +233,18 @@ And `sale.progress.action_cancel()` refuses unless `show_cancel` — *"You can o 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. +**Guard every teardown step on its own.** Wrapping a draft → cancel → unlink sequence in one +`try` is how a purge reports success over residue: `button_draft` raises on an +already-draft invoice, and the `unlink` beneath it never runs. Then run the whole chain in a +loop until a pass keeps nothing — cascades free records that the previous pass could not +touch. + +**Button methods return an action dict Odoo cannot marshal over XML-RPC.** +`action_cancel`, `action_approved`, `action_price_revision`, `create_invoices` and friends +all raise a `Fault` whose text mentions `Marshal`/`dumps`. **The method already ran and +committed** — only the response failed. Catch that specific fault and continue; retrying +re-runs the transition and will fail the second time on a state guard. + --- ## Illustrative dataset shape