Skip to content

fix(spp_drims): stop backorder dispatches bypassing the DRIMS request - #390

Draft
emjay0921 wants to merge 1 commit into
19.0from
fix/1087-drims-backorder-visibility
Draft

fix(spp_drims): stop backorder dispatches bypassing the DRIMS request#390
emjay0921 wants to merge 1 commit into
19.0from
fix/1087-drims-backorder-visibility

Conversation

@emjay0921

Copy link
Copy Markdown
Contributor

Why is this change needed?

Validating a DRIMS dispatch short of its demand and choosing Create Backorder processed the remainder entirely outside the request workflow. Reproduced on a dev instance before changing anything:

  • The request stayed at Dispatched while 10 of 100 units sat in a Ready backorder.
  • Nobody was told: 0 activities, 0 alerts, no chatter entry (message_post appeared nowhere in the module).
  • Odoo builds a backorder with picking.copy(), so every field left at the default copy=True was inherited. Three consequences, all measured:
    • Beneficiary double-count. beneficiary_count was inherited and hazard_incident.py:341 sums it over every done dispatch. Validating the 10-unit backorder moved drims_beneficiaries_served 1500 → 2000 — one 100-unit distribution to 500 people reporting 1000 served.
    • The beneficiary guard was silently bypassed. button_validate() returned True with no prompt, because the inherited value already satisfied it. Nobody ever confirmed where the remaining units went.
    • False departure record. The backorder carried the parent's date_departed and driver_name — a departure timestamp predating its own existence, on goods still in the warehouse.

In a humanitarian context these are false coverage figures and undocumented aid movements, which is what the ticket flags as the real risk.

Not broken, and verified rather than assumed: waybill_number is already copy=False and regenerates, and drims_request_id / drims_request_line_id already carry onto the backorder and its split move — so the request link and per-line attribution were correct. Both are now covered by a regression test instead of being "fixed".

How was the change implemented?

copy=False on the per-shipment facts (stock_picking.py) — beneficiary count, departure/arrival, the pod_* block, transport and driver details, discrepancy notes, drims_return_id. Declarative, so it fixes every copy path rather than just backorders, including the Duplicate action producing a dispatch that claims a delivery. It also makes the existing beneficiary guard fire on the backorder.

Coordinator notification (request.py) — _create_backorder() routes DRIMS dispatch backorders to their request, which posts an internal note and schedules a to-do activity. Coordinators are resolved by mirroring rule_request_coordinator_scope (security/rules.xml), matching users whose drims_area_ids cover the destination area or any ancestor, so the notification reaches exactly those permitted to see the request. Runs sudo() because a warehouse officer may sit outside the request's area scope and this is system bookkeeping, not a user edit.

Request state — reopens at allocated (Ready for Dispatch) while a backorder is pending, and re-advances to dispatched once nothing is outstanding.

Quantity reconciliation (request_line.py, stock_move.py) — action_create_dispatch counts a quantity as dispatched when it is committed to a picking rather than when it ships, so that running total is rebuilt from the moves that still stand on _action_done / _action_cancel. One reconciliation covers a cancelled backorder, a cancelled dispatch, and declining Create Backorder. That last path cancels no move at all — Odoo just drops the excess demand — so a cancellation hook alone would have missed it.

quantity_dispatched keeps its existing "committed to a picking" meaning, so OP#1033's partial-dispatch behaviour is untouched.

New unit tests

spp_drims/tests/test_dispatch_backorder.py — 12 tests:

  • Per-shipment facts not inherited: beneficiary count; departure, arrival, driver, POD.
  • The beneficiary guard fires on the backorder rather than being pre-satisfied.
  • Identity that should carry does: request link, DRIMS type, incident, own unique waybill, per-line attribution across the move split.
  • Request reopens from dispatched while the backorder is pending, and returns to dispatched once it ships.
  • Incident beneficiaries are not double-counted (500 + 40 entered = 540, not 1000).
  • Coordinator gets the note and the activity; no coordinator configured still logs and does not break validation.
  • Quantity released on a cancelled backorder, on declining Create Backorder, and on cancelling the whole dispatch.

Unit tests executed by the author

Full module suite, run after merging the current 19.0:

0 failed, 0 error(s) of 259 tests

All 12 new tests confirmed executing by name in the log rather than inferred from the total. OP#1033's existing partial-dispatch assertions still pass. ./spp lint (ruff + ruff-format) passes on all six files.

Also verified in the running app, not only in tests: the backorder shows beneficiary_count=0, no departure, no driver and its own waybill while keeping request/type/incident/area; the request reopens to allocated; the coordinator receives both the note and an assigned activity; the guard fires; the incident counts 540; and on the cancel path total_dispatched drops to 90 with Create Dispatch succeeding for the remaining 10.

How to test manually

Requires a DRIMS warehouse with stock, and a user in DRIMS District Coordinator whose DRIMS Areas include the request's destination area.

  1. Create a request for 100 units, submit, approve, allocate, then Create Dispatch.
  2. On the dispatch, fill Estimated Beneficiaries Reached (500) and Distribution Area, and click Confirm Departure.
  3. In the Operations tab set Quantity to 90 (demand 100) and click ValidateCreate Backorder.
  4. On the backorder, confirm the DRIMS tab shows no beneficiary count, no departure date and no driver, and that it has its own waybill number.
  5. On the request, confirm the state is back to Ready for Dispatch, the chatter names the backorder, and the coordinator has an activity "Release dispatch backorder …".
  6. Validate the backorder with no beneficiary count — it must refuse. Enter 40 and validate.
  7. Confirm the request returns to Dispatched and the incident dashboard's beneficiaries-served rose by 540 total, not 1000.
  8. Cancel path: repeat steps 1-3, then cancel the backorder. The request must stay actionable — Create Dispatch should offer the remaining 10 units rather than refusing with "Nothing left to dispatch".

Related links

  • OP#1087 — DRIMS: Backorder dispatch bypasses DRIMS request allocation and coordinator visibility

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 against the same module. The entry for this change would be fix(spp_drims): stop backorder dispatches bypassing the DRIMS request at 19.0.2.0.1.

Two adjacent gaps found while investigating, both left alone as out of scope:

  1. quantity_delivered is never written by any production code in the module — only by tests. So total_delivered and fulfillment_pct are permanently 0, and alert.py:276-282's "quantity needed" always reports the full requested amount. That is the delivered side, which OP#1088's Confirm Delivery popup owns.
  2. OP#1057 locks the Operations tab against adding products but will not stop reducing a quantity, which is what triggers a backorder. Complementary to this fix, no overlap.

🤖 Generated with Claude Code

Validating a dispatch short of its demand and choosing "Create Backorder"
left the request reading as fully dispatched, told no one, and attributed
the parent shipment's per-shipment facts to goods still in the warehouse.

Odoo builds a backorder with picking.copy(), so every field left at the
default copy=True was inherited. Mark the per-shipment facts copy=False:
beneficiary count, departure/arrival, the pod_* block, transport and
driver details, discrepancy notes and drims_return_id. This also stops
the Duplicate action producing a dispatch that claims a delivery, and it
makes the beneficiary guard in button_validate() fire on the backorder
instead of being pre-satisfied by the inherited value — which had let one
100-unit distribution to 500 people report 1000 beneficiaries served on
spp.hazard.incident.drims_beneficiaries_served.

Announce the backorder on the request: an internal note plus a to-do
activity for the coordinators of the destination area, resolved by
mirroring rule_request_coordinator_scope so it reaches exactly those
permitted to see the request.

Reopen the request at Ready for Dispatch while a backorder is pending,
and re-advance to Dispatched once nothing is outstanding. Because
action_create_dispatch counts a quantity as dispatched when it is
committed to a picking rather than when it ships, that running total is
now rebuilt from the moves that still stand whenever moves are validated
or cancelled. A single reconciliation covers a cancelled backorder, a
cancelled dispatch, and declining Create Backorder — the last of which
cancels no move at all, it just drops the excess demand, so a
cancellation hook alone would have missed it.

Odoo already carries drims_request_id and drims_request_line_id onto the
backorder and its split move, so the request link and per-line
attribution were correct and are covered by a regression test.

OP#1087
@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.

Comment on lines +798 to +799
self.env["res.users"]
.sudo()
Comment on lines +798 to +799
self.env["res.users"]
.sudo()
edit.
"""
self.ensure_one()
request = self.sudo()
part of it was cancelled rather than shipped has to become actionable
again.
"""
for rec in self.sudo():
Counterpart to ``_on_dispatch_backorder_created``: when the outstanding
backorder is finally validated, the request returns to ``dispatched``.
"""
for rec in self.sudo():
have write access to the request under the area record rules, and this is
system bookkeeping rather than a user edit.
"""
Move = self.env["stock.move"].sudo()
system bookkeeping rather than a user edit.
"""
Move = self.env["stock.move"].sudo()
for line in self.sudo():
):
dispatched += move.quantity if move.state == "done" else move.product_uom_qty
line.quantity_dispatched = dispatched
self.sudo().request_id._reopen_if_not_fully_dispatched()
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.59091% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.30%. Comparing base (7fff93b) to head (bb39f61).
⚠️ Report is 1 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_drims/models/request.py 93.75% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #390      +/-   ##
==========================================
+ Coverage   72.27%   72.30%   +0.03%     
==========================================
  Files        1010     1008       -2     
  Lines       60741    60814      +73     
==========================================
+ Hits        43900    43974      +74     
+ Misses      16841    16840       -1     
Flag Coverage Δ
spp_base_common 91.07% <ø> (ø)
spp_drims 81.48% <96.59%> (+0.65%) ⬆️
spp_drims_sl_demo 68.43% <ø> (-0.05%) ⬇️
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/request_line.py 88.88% <100.00%> (+1.93%) ⬆️
spp_drims/models/stock_move.py 100.00% <100.00%> (ø)
spp_drims/models/stock_picking.py 80.99% <100.00%> (+4.20%) ⬆️
spp_drims/models/request.py 90.30% <93.75%> (+0.65%) ⬆️

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

2 participants