Correct three claims that a live 19.0+e build disproved
Built a full demo against a real BuildEase instance and three documented facts turned out to be wrong, each costing a debugging round. buildease installs nothing. Its manifest depends on base alone, so the "depends on nothing and installs the set" line left an empty database with no error to catch it. Route and install every module explicitly. The purge chain had invoices after 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. The documented order deadlocks on the one statement you invoiced. custom_product_sale, custom_product_bom and bom_on_sale_order_line are absent from mev but present on the Callista demo servers, so "not in the repo" is not the same as "not installable" when pricing. Also records the revision-formula object graph, which no reference covered: the coefficients live on formula.input, not on revision.formula, and that model hangs off either the take-off (contract S, I) or the statement (that period's s, i). That split is what makes "de index van die maand, niet van vandaag" work. Its rows come from an @api.onchange that write() never fires over XML-RPC, and a stale initial_index_value computes a plausible but wildly wrong revision with no guard behind it -- a factor of 65 instead of 1.005 in this build. Plus: percentage is a cumulative fraction not 0-100, index_value truncates to 3 decimals, and button methods return an action dict Odoo cannot marshal over XML-RPC after the transition has already committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
they actually say ("we measure in Excel then re-type it into the quote"), not on module
|
||||||
names they will never utter.
|
names they will never utter.
|
||||||
|
|
||||||
Install the umbrella module `buildease` plus the routed set. `buildease` depends on
|
Install the routed set **explicitly**, module by module. `buildease` is a marker whose
|
||||||
nothing and installs the set.
|
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
|
## Overlay 2 — Solution precedence
|
||||||
|
|
||||||
@@ -74,15 +81,25 @@ BuildEase-shaped config will drive it.
|
|||||||
*Applies at:* **Purge test residue**, replacing the generic chain.
|
*Applies at:* **Purge test residue**, replacing the generic chain.
|
||||||
|
|
||||||
```
|
```
|
||||||
transient wizard rows → statements (cancelled only, newest first) → invoices
|
transient wizard rows → invoices (draft → cancel → unlink)
|
||||||
→ timesheet lines → tasks → projects → sale orders → take-offs → the partner
|
→ 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
|
The generic traps still apply. These are the BuildEase-specific ones, each of which cost
|
||||||
a debugging round:
|
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 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`.
|
`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
|
- **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.
|
`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
|
- A take-off with **no** sale order still cancels and deletes normally: call `cancel()`, which
|
||||||
|
|||||||
+11
-1
@@ -1,7 +1,12 @@
|
|||||||
# BuildEase (callista-tools/mev) — module map & pain routing
|
# BuildEase (callista-tools/mev) — module map & pain routing
|
||||||
|
|
||||||
Odoo **19.0** vertical for construction / contracting. Repo: `git@github.com:callista-tools/mev.git`.
|
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
|
## 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`,
|
- **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
|
`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.
|
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.
|
- **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.
|
- `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
|
Wrong-country client → set `demo_config.json` company/fiscal position explicitly, or skip
|
||||||
|
|||||||
@@ -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`,
|
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.
|
`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
|
## The guards that will stop you
|
||||||
@@ -137,6 +190,17 @@ exists, and doing so will disagree with the order.
|
|||||||
|
|
||||||
## Teardown
|
## 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:
|
Two corrections to the generic chain, both from source:
|
||||||
|
|
||||||
**Deleting the take-off deletes its sale order.** `quantity.take.off.unlink()` calls
|
**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
|
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.
|
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
|
## Illustrative dataset shape
|
||||||
|
|||||||
Reference in New Issue
Block a user