Skip to content

fix(spp_drims): only let a dispatch ship what its request approved - #391

Open
emjay0921 wants to merge 1 commit into
19.0from
fix/1057-drims-dispatch-line-lock
Open

fix(spp_drims): only let a dispatch ship what its request approved#391
emjay0921 wants to merge 1 commit into
19.0from
fix/1057-drims-dispatch-line-lock

Conversation

@emjay0921

Copy link
Copy Markdown
Contributor

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:

Vector Result
Add a Product 50 units of a never-requested item shipped done, drims_request_line_id empty
Unlock (padlock) → raise Demand Demand 100 → 150; 150 units shipped against a request for 100 with 100 allocated

The 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=150 against requested=100, and the unapproved item's unit cost was booked into the incident's drims_distributed_value as 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 of button_validate:

  1. Every live move must trace back to a line of this request.
  2. Nothing may ship beyond that line's quantity_allocated, counting what earlier dispatches already shipped for it.

Two deliberate choices:

  • Keyed on drims_request_line_id, not Odoo's additional flag. additional looks 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 it False and would slip straight past a check based on it. There is a test asserting that precondition so the reasoning is not lost later.
  • Capping at quantity_allocated transitively enforces approval, because _allocate_stock_fifo never allocates beyond quantity_requested. That avoids introducing a second, parallel notion of "the approved quantity" that could drift.

Form lock — the Operations field's options now carry create and delete domains on drims_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 options entries, not bare create=/delete= attributes on the <field> tag. The x2many passes its options dict through as crudOptions (x2ManyField.extractProps) and useActiveActions evaluates 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 — displayRowCreatescanCreateactiveActions.create, and hasX2ManyActionactiveActions.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 readonly on the moves list would have broken that flow. Core already makes product_id readonly unless additional is 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:

  • An added, unapproved product blocks validation and is named in the error.
  • The guard catches moves Odoo never flagged as additional (the RPC/import hole).
  • A move borrowed from another request's line is refused.
  • Demand raised above the allocation is refused; so is over-picking with Demand intact.
  • Full dispatch still validates; partial dispatch still reaches the Create Backorder prompt; a second dispatch after a top-up validates, so the cumulative check does not false-positive.
  • A plain non-DRIMS delivery is unaffected.

Unit tests executed by the author

Full module suite:

0 failed, 0 error(s) of 256 tests

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 since filtered() runs immediately, but rewritten with comprehensions rather than suppressed.

Verified in the running app on a fresh database, not only in tests:

  • both vectors refused, picking stays assigned, nothing ships;
  • full dispatch validates; partial dispatch still returns the backorder wizard;
  • "Add a Product" and the row-delete are gone from a DRIMS dispatch (confirmed visually);
  • Quantity remains editable (is_quantity_done_editable=True, and a write of 90 persists) while Demand stays read-only;
  • a plain delivery and a DRIMS donation receipt both still show "Add a Product" (confirmed visually) — so the lock is scoped to request_dispatch and 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.py builds the whole fixture set on a fresh DB; …-01.py is a 4/4 regression check of the server-side guard.

  1. Open a Ready DRIMS dispatch → Operations tab. "Add a Product" and the row-delete must be absent. Quantity must still be editable; Demand read-only.
  2. Set Quantity below Demand and Validate → you should get Create Backorder?, not an error.
  3. Add a move for an unrequested product over RPC (or XML-RPC) and Validate → refused, naming the product.
  4. Click the padlock to unlock, raise Demand above the allocated quantity, Validate → refused, reporting dispatched vs allocated.
  5. Open a plain delivery under Inventory → Operations, and a DRIMS donation receipt. Both must still show "Add a Product".

Related links

  • OP#1057 — DRIMS: Should not be able to add products during validation of a dispatch

Notes for the reviewer

readme/HISTORY.md and the manifest version are deliberately untouched — per our convention those land on 19.0 after merge, to avoid conflicts between concurrent PRs on the same module. The entry for this change would be fix(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 after super(). 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.py declares PRIORITY_LOW/MEDIUM/HIGH, but the shipped vocabulary data is routine/urgent/critical, and transport modes are road/air/sea/foot rather than truck. Those constants appear to be dead and will mislead whoever trusts them next — worth its own ticket.

🤖 Generated with Claude Code

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
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.28%. Comparing base (7fff93b) to head (9d19703).
⚠️ Report is 1 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #391      +/-   ##
==========================================
+ Coverage   72.27%   72.28%   +0.01%     
==========================================
  Files        1010     1008       -2     
  Lines       60741    60759      +18     
==========================================
+ Hits        43900    43922      +22     
+ Misses      16841    16837       -4     
Flag Coverage Δ
spp_base_common 91.07% <ø> (ø)
spp_drims 81.03% <100.00%> (+0.20%) ⬆️
spp_drims_sl_demo 68.86% <ø> (+0.38%) ⬆️
spp_programs 65.27% <ø> (ø)
spp_registry 87.22% <ø> (ø)
spp_security 69.56% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_drims/models/stock_picking.py 80.30% <100.00%> (+3.51%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant