fix(spp_drims): only let a dispatch ship what its request approved - #391
fix(spp_drims): only let a dispatch ship what its request approved#391emjay0921 wants to merge 4 commits into
Conversation
A dispatch is generated from an approved request, but its Operations tab stayed editable in Ready state, so two things could be smuggled past the approval workflow. Both were reproduced on a dev instance: each validated successfully with no warning. "Add a Product" attached a move no request line asked for, and 50 units of a never-requested item shipped as done. Separately, using the padlock to unlock the picking made Demand editable again, so an approved line went from 100 to 150 and shipped 150 against a request for 100 with 100 allocated. The request line was left reading dispatched=150 against requested=100, and the unapproved item's unit cost was booked into the incident's drims_distributed_value as distributed relief value. Add _check_drims_dispatch_matches_request, called at the top of button_validate: every live move has to trace back to a line of this request, and nothing may ship beyond that line's allocated quantity, counting what earlier dispatches already shipped for it. Allocation is itself capped at the requested quantity by _allocate_stock_fifo, so comparing against quantity_allocated transitively enforces the approved amount rather than introducing a second notion of it. The check is keyed on drims_request_line_id, not Odoo's `additional` flag. `additional` is only set when a line is added through the form, so a move created over RPC or by an import leaves it False and would slip past a check based on it; there is a test asserting that precondition. On the form, lock the line-up through the Operations field's `options`: no "Add a Product" row and no row delete for a request dispatch. These have to be `options` entries rather than bare create/delete attributes on the field — the x2many passes its options dict through as crudOptions and evaluates each entry as a domain against the parent record, whereas bare attributes on a field tag are ignored. Quantity is deliberately left editable, since entering less than Demand is how a partial dispatch and its backorder are produced; making the list readonly wholesale would break that. OP#1057
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 19.0 #391 +/- ##
==========================================
+ Coverage 71.49% 72.35% +0.85%
==========================================
Files 243 1010 +767
Lines 20785 60854 +40069
==========================================
+ Hits 14860 44030 +29170
- Misses 5925 16824 +10899
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…ch-line-lock # Conflicts: # spp_drims/views/stock_picking_views.xml
OP#1079 removed spp.drims.request.source_warehouse_id and made the request line's quantity_allocated a stored compute over allocation rows, so these fixtures no longer built a request at all.
gonzalesedwin1123
left a comment
There was a problem hiding this comment.
This closes a hole that is live on 19.0 right now (OP#1075's readonly went out with PR #396), and it closes it well. What I verified:
- Both design forks are right and both are tested. Keying on
drims_request_line_idrather thanadditionalis correct — and the precondition test proving RPC-created moves aren't flaggedadditionalmeans the reasoning can't silently rot. Capping atquantity_allocated(which_allocate_stock_fifoalready caps at the requested quantity) enforces approval without inventing a second source of truth. - The cumulative check handles the pending-backorder case correctly: done moves count across pickings, this picking's live moves count at their picked quantity, and an unlocked backorder with raised demand is refused against what already shipped.
uom_id.comparefor the float comparison is right. - The
options-domains mechanism is the correct channel — traced as described throughcrudOptions/useActiveActions, and the history (QA catching the bare-attribute attempt) is exactly why the long comment earns its place. - Partial dispatch still reaches the Create Backorder wizard (tested), so this doesn't reintroduce the OP#1075 regression.
- The #390 coexistence claim holds: different hunks of
button_validate; whichever lands second should rebase-check, as you noted.
1. Blocker — version bump (policy now settled)
Edwin has ruled on the convention question your reviewer note raises: version bumps + HISTORY entries land in the PR, with the concrete number assigned at merge time in the queue order he sets — not as post-merge commits on 19.0. (Same answer as on #412: keep bumps in PRs.) The reasoning: the upgrade signal stays atomic with the change it describes — the #414 case, where deploying code without its data would have silently killed the donation flow, is the class of problem this prevents — and it keeps the proposed #441 CI check enforceable. So: please add the bump + your drafted HISTORY line ("only let a dispatch ship what its request approved") when this rebases for merge. With that, this is an approve.
2. Suggestions (non-blocking)
- An arch test pinning the
optionsdict on the Operationsmove_idsfield. The failure mode QA caught on the first attempt — bare attributes silently ignored — is precisely a view regression, and your own arch-test style from #414 (column_invisibletest) fits perfectly here. - The dead-constants find (
PRIORITY_*,truckvs the shippedroutine/urgent/criticalandroad/air/sea/foot) is worth its own ticket as you say — happy to file it if you haven't.
…and an arch test Version bump to 19.0.3.0.2 with its changelog entry, following the convention Edwin settled: bumps and HISTORY entries land in the PR, with the concrete number reconciled at merge time in queue order. An arch test now pins the options dict on the Operations move_ids field. The first attempt at this guard used bare create/delete attributes on the field tag, which Odoo ignores there — QA caught it, nothing failed. A regression would restore the Add a line and trash affordances just as silently, so the test reads the options off the combined arch and asserts both keys are denied and scoped to request dispatches.
|
Thanks — both done. Pushed as 1. Version bump — done. 2. Arch test — added. It reads the 3. The dead constants are already filed and fixed: OP#1165, PR #438 —
|
Why is this change needed?
A DRIMS dispatch is generated from an approved request, but its Operations tab stayed editable in Ready state. Reproduced on a dev instance before changing anything — a request for 100 units of one approved item yielded two ways past the approval workflow, both validating successfully with no warning:
done,drims_request_line_idemptyThe ticket describes the first. The second has the same impact and is covered by the ticket's own Impact section ("inventory records will reflect quantities that don't match the approved request"), so it is fixed here too.
Fallout in both cases: the request line was left reading
dispatched=150againstrequested=100, and the unapproved item's unit cost was booked into the incident'sdrims_distributed_valueas distributed relief value. In a humanitarian context that is unauthorised aid movement plus a corrupted distribution figure.How was the change implemented?
_check_drims_dispatch_matches_request()(stock_picking.py), called at the top ofbutton_validate:quantity_allocated, counting what earlier dispatches already shipped for it.Two deliberate choices:
drims_request_line_id, not Odoo'sadditionalflag.additionallooks like the natural discriminator, but it is only set when a line is added through the form — a move created over RPC or by an import leaves itFalseand would slip straight past a check based on it. There is a test asserting that precondition so the reasoning is not lost later.quantity_allocatedtransitively enforces approval, because_allocate_stock_fifonever allocates beyondquantity_requested. That avoids introducing a second, parallel notion of "the approved quantity" that could drift.Form lock — the Operations field's
optionsnow carrycreateanddeletedomains ondrims_type, removing the "Add a Product" row and the row-delete for a request dispatch.Worth flagging for reviewers, since it is easy to get wrong: these must be
optionsentries, not barecreate=/delete=attributes on the<field>tag. The x2many passes itsoptionsdict through ascrudOptions(x2ManyField.extractProps) anduseActiveActionsevaluates each entry as a domain against the parent record; bare attributes on a field tag are silently ignored, and only the inner<list>honours those, as static booleans. Both rendered controls were traced to their gates —displayRowCreates→canCreate→activeActions.create, andhasX2ManyAction→activeActions.delete(list_renderer.xml:346).Quantity is left editable on purpose. Entering less than Demand is how a partial dispatch and its backorder are produced, so a blanket
readonlyon the moves list would have broken that flow. Core already makesproduct_idreadonly unlessadditionalis set, and Demand readonly until the padlock is used, so the view change stays narrow.New unit tests
spp_drims/tests/test_dispatch_line_lock.py— 9 tests:additional(the RPC/import hole).Unit tests executed by the author
Full module suite:
All 9 new tests confirmed executing by name in the log rather than inferred from the total.
./spp lint(ruff, ruff-format, prettier) passes on all four files. Ruff flagged B023 on the first draft — lambdas capturing loop variables, harmless as written sincefiltered()runs immediately, but rewritten with comprehensions rather than suppressed.Verified in the running app on a fresh database, not only in tests:
assigned, nothing ships;is_quantity_done_editable=True, and a write of 90 persists) while Demand stays read-only;request_dispatchand has not leaked onto ordinary Inventory operations or other DRIMS picking types.How to test manually
Requires a DRIMS warehouse with stock and an approved, allocated request.
demo-scripts/ds-openspp2-op1057-spp_drims-20260803-02.pybuilds the whole fixture set on a fresh DB;…-01.pyis a 4/4 regression check of the server-side guard.Related links
Notes for the reviewer
readme/HISTORY.mdand the manifest version are deliberately untouched — per our convention those land on19.0after merge, to avoid conflicts between concurrent PRs on the same module. The entry for this change would befix(spp_drims): only let a dispatch ship what its request approved.This branch is independent of #390 (OP#1087). Both edit
button_validate, but in different hunks — this one adds a call at the top, #390 adds state settling aftersuper(). They should merge cleanly in either order; whichever lands second may want a quick rebase check.Unrelated defect noticed while building fixtures, left alone:
spp_drims/models/constants.pydeclaresPRIORITY_LOW/MEDIUM/HIGH, but the shipped vocabulary data isroutine/urgent/critical, and transport modes areroad/air/sea/footrather thantruck. Those constants appear to be dead and will mislead whoever trusts them next — worth its own ticket.🤖 Generated with Claude Code