Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aee1c61b5 | ||
|
|
8454aa132e |
@@ -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
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user