Skip to content

feat(spp_drims): collect proof of delivery in a popup off the dispatch - #393

Merged
emjay0921 merged 7 commits into
19.0from
feat/1088-drims-delivery-confirmation
Aug 20, 2026
Merged

feat(spp_drims): collect proof of delivery in a popup off the dispatch#393
emjay0921 merged 7 commits into
19.0from
feat/1088-drims-delivery-confirmation

Conversation

@emjay0921

Copy link
Copy Markdown
Contributor

Why is this change needed?

Three problems on the dispatch form, all in the ticket:

  • Confirm Departure and Confirm Delivery sat in their own column beside Distribution Details, far enough from Departure & Arrival to read as unrelated.
  • Confirm Delivery was available before departure, so an arrival could be logged for goods that had not left.
  • Confirming delivery required typing the receiver's details into the form first. action_confirm_pod then refused with "Please enter the receiver's name" if you had not — the button told you what you should have done rather than letting you do it.

How was the change implemented?

Layout — each button now sits on the row of the field it fills, via <div class="o_row"> inside the Departure & Arrival group.

Ordering — Confirm Delivery is hidden until date_departed is set, with a muted "Confirm departure first" hint in its place.

The ticket asked for the button greyed out, which an Odoo form cannot express: ViewButton's disabled is a component prop and form_compiler.js never wires it to the arch, so invisible is the only declarative option. Flagged rather than silently substituted. The ordering is also enforced on the model — in action_open_delivery_confirmation and in action_confirm_pod — so no path can log an arrival for goods that never left, whatever the UI does.

The popup — new spp.drims.delivery.confirmation.wizard: receiver name/title/ID and delivery status up front, then tabs for Delivered Items, Evidence (signature, GPS, photos) and Notes & Discrepancies. On confirm it writes the existing POD block on the picking, which then becomes readonly="is_pod_confirmed" so the delivery record is not casually edited afterwards.

Delivered quantities — the popup also records what actually arrived per line. This is the part worth a reviewer's attention: nothing in the module wrote spp.drims.request.line.quantity_delivered before this, so total_delivered and fulfillment_pct sat at 0 however much had been delivered, and spp.drims.alert kept reporting the full requested quantity as still needed. Both #390 and #391 flagged this as belonging here. Quantities default to everything dispatched, are capped at it, and accumulate so a request filled by several dispatches totals correctly instead of overwriting.

A bug the tests missed and the fixtures caught

Lines are populated in both default_get and create. default_get alone only covers callers that pass the picking through the context — which is what the UI does, so the suite was green — but passing picking_id in the create() values, the obvious way from a script or over RPC, produced a wizard with no lines that silently recorded nothing. create() only routes missing fields through default_get, so the early return fired. Found by running the demo script rather than by the tests; there is now a test pinning that exact path.

New unit tests

spp_drims/tests/test_delivery_confirmation_wizard.py — 15 tests:

  • The button opens the wizard once departed, and refuses before departure, twice over, or on a non-dispatch picking.
  • Lines pre-fill from the done moves, defaulting to everything arriving — via the context path and via the values path.
  • Confirm writes the full POD block, stamps arrival, and computes the GeoPoint for GIS reporting.
  • Delivered quantities land on the request: fulfillment_pct goes 0 → 100; a short delivery records only what arrived; quantities accumulate across two dispatches.
  • Cannot deliver more than was dispatched, or a negative quantity; arrival cannot precede departure; out-of-range GPS raises rather than being silently dropped by _compute_pod_gps_point; departure cannot be re-recorded after delivery.

Unit tests executed by the author

Full module suite:

0 failed, 0 error(s) of 262 tests

All 15 new tests confirmed executing by name in the log. ./spp lint (ruff, ruff-format, prettier) passes. Module upgrades cleanly; both wizard models registered with 8 ACL rows across manager / officer / warehouse staff / coordinator, mirroring the create-return wizard's grants.

Verified in the running app on a fresh database via demo-scripts/ds-openspp2-op1088-spp_drims-20260804-01.py (7/7, exits non-zero on regression), and the rendering was checked by hand: buttons sit inline with their fields, the popup's quantity column is editable, the POD block populates and locks, and the request's Fulfillment % reads 100.

How to test manually

The script above leaves three dispatches, one per state.

  1. Not departed — Confirm Departure beside Departed At; Arrived At shows no button, only the hint.
  2. Departed, pending — Confirm Delivery beside Arrived At. Click it. Submitting with Received By empty must refuse; a delivered quantity above what was dispatched must refuse. Fill it in and confirm — the POD block populates and locks.
  3. Delivered — POD read-only, both buttons gone, and the request's Fulfillment % reads 100.

Related links

  • OP#1088 — Improve departure and delivery buttons

Notes for the reviewer

Behaviour change: action_confirm_pod now refuses when departure has not been recorded. test_pod_confirmation asserted the old behaviour and is updated for the new ordering. Anything calling that method directly needs departure first. The method is kept for programmatic callers; the form routes through action_open_delivery_confirmation.

Deliberately out of scope: the request state machine is still unwired. A fully delivered request stays at dispatched even now that Fulfillment reads 100%, because nothing sets the delivered or fulfilled states — and the request-states vocabulary contains both with no documented distinction between them. That is a design decision rather than an oversight, so it wants its own ticket instead of being folded in here.

readme/HISTORY.md and the manifest version are untouched — per our convention those land on 19.0 after merge, to avoid conflicts between concurrent PRs on the same module.

Overlap with the other open DRIMS PRs: this touches the DRIMS tab of stock_picking_views.xml and action_confirm_pod/action_confirm_departure in stock_picking.py. #391 (OP#1057) touches the Operations field and button_validate; #392 (OP#1086) touches the header statusbars; #390 (OP#1087) touches field definitions and button_validate. Different hunks throughout, so they should merge in any order, but this is the largest of the four and may want a rebase check if it lands last.

🤖 Generated with Claude Code

The Confirm Departure and Confirm Delivery buttons sat in their own column
beside Distribution Details, far enough from Departure & Arrival to read as
unrelated. Each now sits on the row of the field it fills.

Confirm Delivery stays hidden until departure is recorded, with a hint in
its place. The ticket asked for it greyed out, which an Odoo form cannot
express: ViewButton's `disabled` is a component prop and is never wired to
the arch, so `invisible` is the only declarative option. The ordering is
enforced on the model as well, in action_open_delivery_confirmation and
action_confirm_pod, so no path can log an arrival for goods that never
left.

Confirming delivery now opens spp.drims.delivery.confirmation.wizard
instead of expecting the officer to have typed the receiver's details into
the form and then pressing a button that refused if they had not. The
wizard collects receiver, delivery status, signature, photos, GPS and
notes, writes them back to the POD block, and locks that block once
confirmed so the delivery record is not casually edited afterwards.

It also records what actually arrived per line. Nothing in the module
wrote spp.drims.request.line.quantity_delivered before this, so
total_delivered and fulfillment_pct sat at 0 however much had been
delivered, and spp.drims.alert kept reporting the full requested quantity
as still needed. Quantities default to everything dispatched, are capped
at it, and accumulate so a request filled by several dispatches totals
correctly rather than overwriting.

Lines are populated in both default_get and create. default_get alone only
covers callers that pass the picking through the context; passing
picking_id in the values — the obvious way from a script or over RPC —
produced a wizard with no lines that silently recorded nothing. A test
pins that path.

test_pod_confirmation asserted the old behaviour of confirming delivery
with no departure recorded, and is updated for the new ordering.

OP#1088
@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 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.33028% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.54%. Comparing base (7ea3452) to head (fcbea5e).

Files with missing lines Patch % Lines
spp_drims/wizard/delivery_confirmation_wizard.py 95.69% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #393      +/-   ##
==========================================
- Coverage   72.72%   72.54%   -0.19%     
==========================================
  Files         447     1012     +565     
  Lines       31339    61220   +29881     
==========================================
+ Hits        22792    44411   +21619     
- Misses       8547    16809    +8262     
Flag Coverage Δ
spp_base_common 91.07% <ø> (ø)
spp_dci ?
spp_dci_client ?
spp_dci_client_compliance ?
spp_dci_client_crvs ?
spp_drims 85.42% <96.33%> (+0.53%) ⬆️
spp_drims_sl_demo 68.47% <ø> (ø)
spp_grm_cel ?
spp_key_management ?
spp_programs 65.27% <ø> (ø)
spp_registry 87.22% <ø> (+0.07%) ⬆️
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 87.64% <100.00%> (+2.48%) ⬆️
spp_drims/wizard/__init__.py 100.00% <100.00%> (ø)
spp_drims/wizard/delivery_confirmation_wizard.py 95.69% <95.69%> (ø)

... and 708 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.

Confirming a delivery from the popup failed with "Expected singleton:
uom.uom()". The wizard line held its move, request line, product, unit and
dispatched quantity as plain readonly fields, and the web client does not
send readonly fields back when it saves, so a confirmation arrived carrying
nothing but quantity_delivered. The empty unit turned uom_id.compare into a
bare singleton error.

The crash was the fortunate part. The request line came back empty too, so
had the check been more forgiving the confirmation would have recorded no
delivered quantities at all and left fulfillment reading 0 - the very
number this wizard exists to write.

Derive the line from its move instead: move_id is required and writable,
and the request line, product, unit and dispatched quantity are related off
it, so the move is the only value that has to survive the round trip. The
quantity check also falls back to float_compare, so a missing unit can
never produce a raw singleton error again.

Move the departure and delivery buttons left as well, by hiding each date
while it is still empty. An empty datetime widget was pushing the button
across the row. The field and its button are mutually exclusive: an unset
date means there is an action to take, a set one means it is done.

Round 1 passed every server-side test and still shipped broken, because the
tests built the wizard themselves with all values supplied. The new test
sends the payload the browser actually sends, and a second one guards
move_id against going readonly again.
PR #396 landed on 19.0 after this branch last merged up. OP#1079 removed
spp.drims.request.source_warehouse_id and made the request line's
quantity_allocated a stored compute over allocation rows, so the fixture no
longer built a request at all and took the whole delivery-confirmation class
with it.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same standard as #390 and #391 — the rigor is appreciated and it verified out. What I checked:

  • The delivered-total arithmetic is sound: per-line cap at the move's dispatched quantity, once-per-picking via the is_pod_confirmed guard, and accumulation onto the request line — so the total can't exceed what was dispatched, across any number of dispatches. And quantity_delivered is a plain writable Float, so this isn't the stored-compute trap #390 fixed on the dispatch side.
  • The round-2 move_id derivation is the correct fix — the web client not sending readonly fields back is exactly the kind of thing that silently zeroes a feature, and deriving product/uom/request-line from the move closes it structurally.
  • Both line-population paths covered (default_get for the context path, create for the values path), with the values-path bug pinned by a test. Finding it via fixtures and documenting it honestly is the right craft.
  • The GPS constraint rescuing _compute_pod_gps_point's silent drop, the model-side ordering guards, and deferring the delivered/fulfilled state-machine question to its own ticket are all right.

1. Blocker — version bump (policy settled)

As ruled on #391: bump + HISTORY land in the PR, number assigned at merge-time rebase in Edwin's queue order. Please add them when this rebases for merge.

2. Blocker — the wizard on a departed-but-unvalidated dispatch records nothing, then locks

action_open_delivery_confirmation checks type / departed / not-confirmed — but not state == 'done' — and _prepare_line_commands only reads done moves. The gap sits exactly between two of your own PRs' flow assumptions: #390's documented flow (and its tests) confirm departure before validating, while this PR's tests always validate first. An officer following #390's order who clicks Confirm Delivery before Validate gets a zero-line wizard; confirming writes the POD block, sets is_pod_confirmed, records no delivered quantities — and the already-confirmed guard then prevents ever recording them. Fulfillment stays 0 with no recovery path short of manual field surgery.

Two small fixes, ideally both: require state == 'done' in action_open_delivery_confirmation ("Validate the transfer before confirming delivery"), and refuse action_confirm when line_ids is empty. A test pinning the departed-but-unvalidated path would keep the two flows honest against each other.

3. Nit — photo provenance

pod_photo_ids uploads through many2many_binary on the wizard form, so the attachments carry res_model = spp.drims.delivery.confirmation.wizard. They survive transient vacuum, but the provenance is wrong and access flows through transient-model semantics. Re-pointing res_model/res_id to the picking in action_confirm keeps the evidence record clean.

Both follow-up tickets you've named across these PRs (the request state-machine design question, and #391's dead constants) are being filed on OpenProject so they don't evaporate.

…alidated

Version bump with its changelog entry, per the settled convention.

The wizard's lines come from done moves, so on a departed-but-unvalidated
dispatch it opened empty — and confirming an empty wizard wrote the proof of
delivery, set is_pod_confirmed and recorded no delivered quantities, after which
the already-confirmed guard prevented ever recording them. Fulfilment stayed at
zero with no way back short of editing fields by hand.

The trap is reachable by following OP#1087's documented order, which confirms
departure before validating. Two guards rather than one: the opener now requires
state 'done', and action_confirm refuses when there are no lines, so the direct
API cannot burn the one-shot flag either.

Delivery photos upload through the wizard form, so they were filed against the
transient model. They are re-pointed to the picking on confirm, which is where
the evidence belongs and whose access rules should govern it.
@emjay0921

Copy link
Copy Markdown
Contributor Author

Thanks — both blockers and the nit are done. Pushed as f3fede6a.

1. Version bump — 19.0.3.0.3 with its HISTORY line.

2. The departed-but-unvalidated trap — fixed, and it was worse than a poor error message. You traced it exactly: an officer following #390's documented order gets a zero-line wizard, and confirming it writes the POD block, burns the one-shot is_pod_confirmed flag and records nothing, leaving fulfilment at zero with no way back. Both guards are in, as you suggested:

  • action_open_delivery_confirmation now requires state == 'done' — "Validate the transfer before confirming delivery."
  • action_confirm refuses an empty line_ids, so the direct API cannot burn the flag either.

Three tests: the departed-but-unvalidated path through the button, the empty-lines path straight at the wizard (asserting the flag is not burned), and the photo provenance below. The first one is the one that keeps the two PRs' flow assumptions honest against each other — it builds a picking in exactly the state #390's flow produces.

3. Photo provenance — re-pointed. pod_photo_ids now have their res_model/res_id moved to the picking in action_confirm, so the evidence is filed against the dispatch and its access follows the picking rather than transient-model semantics. Tested.

spp_drims suite: 284 tests, 0 failed (three new). pre-commit clean.

On the two follow-up tickets: the dead constants are already filed and fixed as OP#1165 / PR #438. The request state-machine design question is still unfiled — happy for you to raise it, or I will.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — both blockers and the nit are fixed, each with its own pinning test.

Verified on f3fede6a:

  • The zero-line lock is closed with belt and braces: action_open_delivery_confirmation now requires state == 'done' (with the comment naming the exact trap — reachable by following #390's documented depart-then-validate order), and action_confirm independently refuses an empty wizard so a direct API construction can't burn the one-shot is_pod_confirmed flag either. The two new tests pin both layers, and the no-lines test asserts the flag is not burned — which is the harm that mattered.
  • Photo provenance re-pointed: attachments move to res_model = stock.picking / the dispatch's id on confirm, with a test asserting both. Evidence now lives where its access rules should govern it.
  • Bump to 19.0.3.0.3 with its HISTORY entry, slotting into the queue's number line (#390 → .1, #391 → .2, this → .3, #433 → .4, #416 → 3.1.0, #414 → 4.0.0) — reconciled at merge time per the settled convention.

CI fully green. The follow-ups born from this PR remain tracked: OP#1174 (the delivered/fulfilled state-machine question) stands, and #433 remains the rebase-heavy neighbour whichever of the two lands second, since both rework the same DRIMS-tab block. Merge stays with Edwin per the usual flow.

Renumbered to 19.0.4.0.2, above the 19.0.4.0.1 #391 just landed.

The manifest conflicted twice — the version and the data-file list, where each
branch had registered its own wizard view; both views are kept. tests/__init__
and wizard/__init__ likewise take every module each side added.

stock_picking_views.xml merged cleanly despite the DRIMS-tab rename #433 made,
and the delivery-confirmation button still sits inside the renamed tab.
@emjay0921
emjay0921 merged commit b3c574f into 19.0 Aug 20, 2026
22 checks passed
@emjay0921
emjay0921 deleted the feat/1088-drims-delivery-confirmation branch August 20, 2026 03:54
gonzalesedwin1123 added a commit that referenced this pull request Aug 20, 2026
Drift back-merge per the staging plan (19.0 advanced 11 commits since the
batch-2 cut: #303, #322, #323, #390, #391, #393, #411, #412, #414, #416,
#433). Conflicts were confined to spp_farmer_registry_demo metadata:
batch 2's #336 demo fix claimed version 19.0.2.1.2, which 19.0 has since
consumed (#412=.2, #322=.3, #323=.4). Resolution re-bumps the batch-2
farmer-demo change to 19.0.2.1.5 with its HISTORY entry moved on top;
README.rst/index.html regenerated via the pinned oca-gen hook. The
change ships no migration directory, so the collision was metadata-only.
The generator code change itself auto-merged cleanly.
emjay0921 added a commit that referenced this pull request Aug 20, 2026
Adds the version bump the review asked for — 19.0.4.0.3, above the 19.0.4.0.2
#393 just landed — with its changelog entry.

stock_picking_views.xml needed rebuilding rather than merging: #433, #391 and
this branch all insert xpath blocks into the same region, and git's split of the
conflict cut through an <xpath> element, so a mechanical resolution produced
malformed XML. Took 19.0's file and re-applied this branch's own insertion — the
OP#1086 comment plus the two statusbar xpaths — after the DRIMS button block.
Verified as a pure insertion: no removals against 19.0, and the file parses.

tests/__init__ takes every test module the branches registered.
emjay0921 added a commit that referenced this pull request Aug 20, 2026
The review's one blocker: 19.0.4.0.3, above the 19.0.4.0.2 #393 landed, with its
changelog entry and the regenerated README.
emjay0921 added a commit that referenced this pull request Aug 20, 2026
…#392)

* fix(spp_drims): show only reachable states on the dispatch status bar

action_create_dispatch confirms a request dispatch the moment it creates
it, so a dispatch never sits in Draft. Both Draft and Waiting were still
drawn as greyed-out steps on the form, inherited from stock.picking.

Core renders two status bars for stock.picking, split on
picking_type_code. A dispatch is outgoing, so it picks up the non-incoming
one with draft,confirmed,assigned,done. Narrow that bar to exclude
dispatches and add a dispatch-only bar showing assigned,done, rather than
editing the shared statusbar_visible — which would have dropped Draft from
every non-incoming transfer in the database. The three bars' conditions
are mutually exclusive, so exactly one renders per picking.

Waiting is hidden as a *future* step only, and deliberately so: contrary
to the ticket's rationale it is genuinely reachable. DRIMS allocation
writes quantity_allocated on the request line and creates no Odoo
reservation, so two requests can allocate the same units and whichever
dispatches second has nothing to reserve and lands in confirmed. That is
safe here because the statusbar widget always renders the current value
even when it is excluded from statusbar_visible (getAllItems filters on
`value === currentValue || visibleSelection.includes(value)`), so a
dispatch short of stock still shows Waiting to warehouse staff.

Both facts are covered by tests so neither gets "simplified" later on a
false assumption, and the mutual exclusivity test evaluates the real
invisible expressions per picking kind rather than matching strings.

OP#1086

* test(spp_drims): follow the per-warehouse allocation model

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.

* test(drims): produce Waiting by losing the stock, not by double-allocating

test_waiting_state_is_reachable_for_a_dispatch built its Waiting dispatch by
allocating the same stock to two requests. Since PR #396 (OP#1079) that is
refused - _drims_available_quantity subtracts pending allocations, so stock
cannot be promised twice - and the test errored on its own setup.

The ticket's reasoning still holds. Allocation records per-warehouse rows and
reserves nothing, so it cannot guarantee the stock is still there when the
dispatch is created: an inventory adjustment or transfer in between leaves
the dispatch with nothing to reserve and it lands in confirmed. That is why
Waiting is hidden only as a future step, never as the current value.

Rebuilt around that instead. No production code changes.

* fix(spp_drims): bump version for the dispatch status bar change

The review's one blocker: 19.0.4.0.3, above the 19.0.4.0.2 #393 landed, with its
changelog entry and the regenerated README.

* docs(spp_drims): regenerate README for the 19.0.4.0.3 entry
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.

2 participants