Skip to content

feat(spp_drims): Incident Management review — states, closed-incident limits, dashboard and warehouses - #416

Open
emjay0921 wants to merge 16 commits into
19.0from
feat/1100-drims-incident-management
Open

feat(spp_drims): Incident Management review — states, closed-incident limits, dashboard and warehouses#416
emjay0921 wants to merge 16 commits into
19.0from
feat/1100-drims-incident-management

Conversation

@emjay0921

@emjay0921 emjay0921 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Why is this change needed?

OP#1100 (DRIMS - Full review - Incident Management) and all of its children have passed QA — this raises the PR for that work, per the flow of opening PRs once QA signs off.

Ticket Status
OP#1094 Invalidate stock KPI cache when warehouses change Closed
OP#1123 Dashboard KPI boxes navigate unexpectedly Test pass
OP#1157 Enable "Alert" state for incidents Test pass
OP#1158 Limit possible actions on incidents in state "Closed" Test pass
OP#1159 Hide "Impact" tab on Incidents Test pass
OP#1160 Changes to Incident Dashboard view Test pass
OP#1164 Add warehouses to an incident Test pass

What is in here

  • Incident lifecycle (OP#1157) — an incident is now entered as a Draft and classified from there, with Flag As Alert and Set Active offered side by side. The lifecycle is Draft → Alert or Active → Recovery → Closed, and Alert stays reachable later. Draft was added to the list's default filter, or a newly recorded incident would have disappeared from the list it was created in.
  • Closed incidents are inert (OP#1158) — submitting, approving and allocating requests, accepting donations, deploying personnel and editing incident fields are all refused once an incident is closed, and the Hazard Category no longer links out.
  • Incident warehouses (OP#1164) — warehouses and incidents are linked bidirectionally, and the donation and request pickers filter to the incident's warehouses.
  • Dashboard (OP#1160, OP#1123, OP#1094) — Units/Products counts incident stock net of allocations and Distributed is net of returns; clicking a KPI box no longer navigates away, only the incident name opens the form; and the stock-value KPI refreshes when a warehouse's incidents change.
  • Impact tab hidden (OP#1159) pending OP#1155.

Closed incidents: what is enforced where

Enforced on the server: the DRIMS operations (_drims_ensure_open), and the incident's own lifecycle — a draft cannot be closed, and a closed incident cannot be moved back to alert, active or recovery.

UI only, deliberately: editing the incident's own fields while closed. A write() guard would have to allowlist its way around the stored KPI computes, which legitimately write to closed incidents — this PR's own migration recomputes them, and so would any future repair. Blocking writes there would break the mechanism that keeps a closed incident's numbers honest, to prevent an edit that carries no operational consequence. The data that matters is protected by the operation guards.

Version bumps: spp_drims to 19.0.3.1.0 and spp_hazard to 19.0.2.1.0, with migrations/19.0.3.1.0/post-migration.py. Three stored computes changed meaning rather than being added, so an upgrade alone would leave existing rows on the old semantics — indefinitely for closed incidents, which the refresh cron skips. The migration purges the stale spp.data.value cache rows first (the computes prefer a live cache entry) and then recomputes all three for every incident.

Unit tests executed by the author

On the merged branch, across the base module and everything downstream of it:

spp_hazard             0 failed, 0 error(s) of  71 tests
spp_hazard_programs    0 failed, 0 error(s) of  24 tests
spp_drims              0 failed, 0 error(s) of 284 tests
spp_drims_sl           0 failed, 0 error(s) of   3 tests
spp_drims_sl_demo      0 failed, 0 error(s) of   8 tests

./spp lint --all-files clean.

How to test manually

  1. Create an incident — it saves as Draft and offers exactly Flag As Alert and Set Active. Confirm it still appears in the incident list.
  2. Classify it either way, then walk Active → Recovery → Closed.
  3. On a closed incident: its fields are read-only, Hazard Category is plain text rather than a link, and a request or donation against it is refused.
  4. Link a warehouse to an incident and confirm the donation and request pickers narrow to it.
  5. On the dashboard, click a KPI box — it must not navigate; only the incident name opens the form.

Related links

https://openspp.openproject.com/work_packages/1100

Reviewer notes

OP#1157 changed requirement twice, which is why its history on this branch is three commits. Round 1 exposed the existing Alert state via a button; round 2 made Alert the entry state; round 3 replaced that with a new Draft state, because QA wanted the person recording an incident to say what it is rather than have Alert assumed. The final shape is round 3.

Two decisions were made where the ticket was silent, both confirmed by QA on the ticket: a draft cannot be closed — a mistaken one is deleted — and a draft does not count as a live incident anywhere (not "ongoing", not marking registrants affected, and not offered where only Active incidents are, such as low-stock alerting and the request-from-template picker). It starts counting once classified.

spp_hazard is a base module with dependants, so the runs above cover both modules that inherit the incident form, not just the module under change.

The branch was brought up to date by merging 19.0, not rebasing, since it was already published.

emjay0921 added 14 commits July 24, 2026 12:13
…of allocations) and Distributed net of returns (#1160)
QA round 1 on OP#1157 returned three findings and a question.

Flag As Alert was inserted before the statusbar, which rendered it last.
It is now anchored on the first header button so the order reads Flag As
Alert, Start Recovery, Close Incident — the order the workflow runs in.
Anchored by button name rather than position, so a reordering of the base
view fails loudly at upgrade instead of silently drifting back.

New incidents landed straight in Active, skipping the triage step the
Alert state exists for. The status default becomes "alert", making the
lifecycle Alert -> Active -> Recovery -> Closed. Changed on spp_hazard,
where the state machine lives, so every consumer behaves the same rather
than the same model behaving differently depending on which modules are
installed.

Close Incident was already correct in effect — with four states,
"not closed" and "alert, active or recovery" are the same set — but it is
now stated positively so it stays right if a state is ever added.

QA also asked when Recovery can be set: only from Active, since Start
Recovery is hidden otherwise. With Alert as the entry state a new incident
is confirmed Active before Recovery is offered. A test pins that so the
answer does not quietly change.

Four spp_hazard tests asserted the old default; they now confirm Active
explicitly rather than assuming it.

Not changed, and flagged for QA instead: DRIMS low-stock alerting and the
request-from-template picker both filter on status = "active", so an
incident sitting in Alert reaches neither until it is confirmed. Whether an
alert-state incident should drive stock alerting is a product decision.

Verified across every spp_hazard dependent — spp_hazard, spp_drims,
spp_hazard_programs, spp_api_v2_gis, spp_gis_indicators, spp_drims_sl_demo
— and by hand on a fresh database.

OP#1157
Closing an incident left Hazard Category clickable, so a closed record
still offered a way through to the category form. Making the field
readonly does not cover this — a readonly many2one still renders as an
internal link — and no_open cannot be made conditional, because options is
parsed as a static dict and cannot reference status.

Declare the field twice instead, with mutually exclusive invisible: an open
incident keeps its link, a closed one is inert text. The same pattern is
already used across the codebase wherever a field has to change shape with
state.

Asserted on the arch rather than through behaviour, since whether a
many2one is clickable is decided in the client, not the ORM.
…ent-management

# Conflicts:
#	spp_drims/models/request.py
#	spp_drims/views/request_views.xml
OP#1079 made the request line's quantity_allocated a stored compute over
per-warehouse allocation rows, so the helper's direct write no longer
registered and the Units KPI counted stock that had in fact been allocated.
Round 2 made Alert the entry state. QA has since changed the requirement: an
incident should be entered as a draft, and the person recording it says what
it is — Flag As Alert for something being watched, Set Active for a response
already under way. Neither is assumed on their behalf.

Adds a draft state at the head of the selection and makes it the default.
Set Active is now offered from Draft as well, so a response already under way
does not have to be flagged as an alert first. Start Recovery and Close
Incident stay hidden there, which is what leaves a draft showing exactly the
two buttons asked for.

The list's default filter needed Draft adding too. It pre-selects Alert,
Active and Recovery, so with Draft as the entry state a newly created
incident would have disappeared from the list it was created in and read as
having failed to save. A test asserts both the filter and its default,
because nothing at the model level would notice.

A draft is deliberately not a live incident: it is excluded from is_ongoing,
from the affected-registrant check, and from the Active-only consumers. It
starts counting once classified. A draft also cannot be closed — a mistaken
one is deleted rather than closed.

Six tests asserting the round-2 lifecycle are rewritten, not dropped.
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.24771% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.85%. Comparing base (0820667) to head (f559229).

Files with missing lines Patch % Lines
spp_hazard/models/hazard_incident.py 80.00% 2 Missing ⚠️
spp_drims/models/hazard_incident.py 98.21% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #416      +/-   ##
==========================================
+ Coverage   72.24%   72.85%   +0.60%     
==========================================
  Files         419      464      +45     
  Lines       29813    31575    +1762     
==========================================
+ Hits        21539    23003    +1464     
- Misses       8274     8572     +298     
Flag Coverage Δ
spp_api_v2_change_request 66.53% <ø> (ø)
spp_api_v2_cycles 71.03% <ø> (ø)
spp_api_v2_entitlements 70.23% <ø> (ø)
spp_api_v2_gis 71.57% <ø> (ø)
spp_api_v2_programs 92.22% <ø> (ø)
spp_api_v2_simulation 71.19% <ø> (?)
spp_base_common 91.07% <ø> (ø)
spp_case_entitlements 100.00% <ø> (?)
spp_case_programs 100.00% <ø> (?)
spp_change_request_v2 77.83% <ø> (ø)
spp_cr_type_assign_program 92.07% <ø> (ø)
spp_dci_compliance 93.01% <ø> (?)
spp_drims 83.38% <98.98%> (+1.13%) ⬆️
spp_hazard 99.20% <80.00%> (?)
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/donation.py 97.00% <100.00%> (+0.09%) ⬆️
spp_drims/models/personnel.py 98.41% <100.00%> (+0.16%) ⬆️
spp_drims/models/request.py 93.37% <100.00%> (+0.18%) ⬆️
spp_drims/models/res_config_settings.py 63.79% <100.00%> (+2.68%) ⬆️
spp_drims/models/returns.py 93.06% <ø> (ø)
spp_drims/models/stock_warehouse.py 77.94% <100.00%> (+7.35%) ⬆️
spp_drims/models/hazard_incident.py 76.27% <98.21%> (+10.99%) ⬆️
spp_hazard/models/hazard_incident.py 98.05% <80.00%> (ø)

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

@emjay0921
emjay0921 marked this pull request as ready for review August 14, 2026 03:36

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

Thanks — this is a well-built PR. The tests are genuinely good (arch-level assertions with rationale, both contributing modules covered, guards tested positive and negative), and I verified the OP#1123 kanban approach against the Odoo 19 source: can_open="0" on <kanban> plus <a type="open"> is correct — type="open" calls openRecord unconditionally (kanban_record.js:346), independent of canOpenRecords. Routing _inverse_drims_warehouses through stock.warehouse.write so the OP#1094 refresh hook fires is a nice touch.

Two things need changes before this can land, plus some questions and nits.

1. Blocker — no version bump and no migration on either module

spp_drims stays at 19.0.3.0.0 and spp_hazard at 19.0.2.0.2, identical to 19.0. Both modules shipped in release 2026.08, so the released-baseline rule applies (same-version code changes give upgraders no signal and skip migrations/ scripts).

On an existing database without an upgrade:

  • The new ir.config_parameter record drims.warehouse.filter_by_incident never loads (the code default masks this one).
  • None of the view changes load — Draft filter/statusbar, Flag As Alert, closed-incident readonly, kanban can_open — i.e. the bulk of the user-visible work.

And even with an upgrade, stored computes whose semantics changed are not recomputed — Odoo only auto-computes stored fields when the column is newly created:

  • drims_total_stock_units / drims_stock_item_count changed meaning from "physical warehouse contents" to "incident-related stock net of allocations". Existing rows keep old-semantics numbers until a dependency happens to change — for quiet or closed incidents, indefinitely.
  • drims_distributed_value is now net of returns. Stored values and live spp.data.value cache rows still hold gross numbers. The 15-minute cron self-heals open incidents, but closed incidents are excluded from the cron and never recompute.

Ask: bump both manifests and add a migrations/<version>/post-migration.py that purges the drims_distributed_value (and arguably drims_stock_value) cache rows and recomputes the three stored KPI fields for all incidents.

2. Major — "closed is inert" is UI-only for the incident itself; lifecycle transitions are unguarded server-side

The DRIMS operations got real server-side guards (_drims_ensure_open), but the incident's own lifecycle didn't:

  • action_close, action_set_active, and the new action_set_alert have no state guards — visibility is only enforced in the form header. Via RPC/import/shell, a draft can be closed (contradicting the QA-confirmed "a draft cannot be closed — a mistaken one is deleted") and a closed incident can be flipped back to active/alert, sidestepping every OP#1158 guard.
  • Field edits on a closed incident are only readonly= in the views, which Odoo does not enforce on write — so "editing incident fields are all refused" holds only in the web client.

Ask: at minimum, raise in the action_* methods when the transition isn't legal from the current state (e.g. action_close refuses drafts, action_set_alert/action_set_active refuse closed). Optionally a write() guard on closed incidents (allowlisting status/end_date/chatter fields) if server-side enforcement of field edits is wanted. If UI-only is the accepted posture for field edits, let's state that explicitly in the PR description instead.

3. Questions — gaps in the closed-incident guard coverage (possibly intentional)

  • action_create_dispatch is not guarded: a request allocated before closure can still be dispatched afterwards — stock leaves the warehouse for a closed incident. Allocate is guarded; dispatch isn't. Intentional?
  • action_mark_received / action_stock on donations are not guarded: a donation pledged before closure can be received and stocked in afterwards. Deliberate (goods in flight), or a gap?
  • The create-time guards check vals only, so write({'incident_id': <closed>}) re-points an existing donation/personnel record to a closed incident unguarded.
  • A request can still be created against a closed incident (only submit/approve/allocate raise) while a donation cannot even be created — the guard tests themselves rely on creating requests against a closed incident. Is the asymmetry intended?

4. Nits

  • spp_hazard/tests/test_registrant.py — comment says "incidents now start in alert"; they start in draft.
  • _cron_refresh_drims_kpis — the "status field values: alert, active, recovery, closed" comment is missing draft.
  • _compute_allowed_warehouse_ids is duplicated verbatim (docstring included) in donation.py and request.py; a small shared helper would keep the filter-on/fallback policy in one place.
  • Units/Distributed asymmetry: a confirmed return reduces Distributed, but the returned goods are not added back to incident Units (their allocation still counts them out; re-allocating them would count them out twice, clamped at 0 per product). Probably fine — deserves a note.
  • In base spp_hazard without DRIMS installed, a draft has no UI path to Alert (the button lives in spp_drims) — only Set Active. Fine if Alert is a DRIMS-driven concept, but the state lives in the base module.

Both warehouse-choice tests create a donation with no items. The donations
review on fix/1076-drims-donations-review adds _check_has_lines, which forbids
exactly that — so whichever of the two branches merges second turns 19.0 red,
and with no textual conflict between them nothing warns either author first.

The line is incidental to what these tests assert; it is there to keep the
donation valid.

Not done here: dropping this branch's create-time closed-incident guard in
favour of that review's _check_incident_not_closed constraint, which is
strictly broader because it also catches an existing donation re-pointed at a
closed incident. The constraint does not exist on this branch, so removing the
guard now would leave test_1158_donation_blocked_when_closed with nothing to
raise. It should follow once the donations PR has merged.
… and lifecycle guards

Version bumps on spp_drims and spp_hazard, and a migration for the part a bump
alone does not fix. Three stored computes changed meaning rather than being
added: incident stock units and item count now count incident-related stock net
of allocations, and distributed value is net of confirmed returns. Odoo only
computes a stored field for existing rows when its column is new, so an upgraded
database would keep the old numbers until some dependency happened to change —
and for closed incidents never, since the refresh cron skips them. The migration
drops the stale spp.data.value cache rows the computes would otherwise prefer,
then recomputes all three for every incident, closed ones included.

The incident's own lifecycle is now enforced on the server, not only hidden in
the form header. A draft cannot be closed — QA's rule is that a mistakenly
entered incident is deleted — and a closed incident cannot be moved back to
alert, active or recovery, which over RPC would have sidestepped every guard
that keys off `closed`. Alert goes through the same gate.

Field edits on a closed incident stay a UI-level rule, deliberately: a write()
guard would have to allowlist its way around the stored KPI computes, which
legitimately write to closed incidents. The rule that protects data is on the
DRIMS operations, and those are already guarded server-side.

Three spp_hazard tests closed an incident straight after creating it, which the
draft entry state makes illegal; they now take it through the lifecycle first.

Nits from the review: _compute_allowed_warehouse_ids was duplicated verbatim in
donation.py and request.py and now calls one helper on the incident, the cron
comment listing status values was missing draft, and a test comment still said
incidents start in alert.
@emjay0921

Copy link
Copy Markdown
Contributor Author

Thanks — blockers 1 and 2 are done, the nits are done, and the questions in 3 are answered below with one of them fixed. Pushed as f5592290 (plus c433ad82 earlier for the #414 fixture collision).

1. Version bump + migration — done. spp_drims 19.0.3.0.019.0.3.1.0, spp_hazard 19.0.2.0.219.0.2.1.0, both with changelog entries, and spp_drims/migrations/19.0.3.1.0/post-migration.py.

The migration does the two things you identified, and the second is the one I would have missed: it drops the drims_distributed_value and drims_stock_value cache rows first — because the compute prefers a live cache entry, so recomputing without purging would have read the pre-change numbers straight back — then recomputes drims_total_stock_units, drims_stock_item_count and drims_distributed_value for every incident via add_to_compute. Deliberately not filtered to open incidents: closed ones are exactly where a stale number would sit forever, since the cron skips them.

2. Server-side lifecycle guards — done. _ensure_status_change_allowed on spp.hazard.incident, called by action_close, action_set_active, action_set_recovery and (from spp_drims) action_set_alert. Two rules, both from the lifecycle documented on status: a draft is not closed, and nothing moves out of closed.

On the optional write() guard, I went with your "state the posture explicitly" branch, and there is a concrete reason rather than just cost: a blanket guard on closed incidents would have to allowlist its way around the stored KPI computes, which legitimately write to closed incidents — the migration above does exactly that, and so would any future recompute. Blocking writes there would break the mechanism that keeps closed incidents' numbers honest. So field edits stay a UI rule and the enforcement lives on the DRIMS operations, which have real guards. Now said in the PR body too.

Three spp_hazard tests closed an incident immediately after creating it, which the draft entry state makes illegal — they now go through the lifecycle first. That is the guard working, but worth flagging as a behaviour change for anyone with their own fixtures.

3. Your questions:

4. Nits — all done except the two that are notes rather than changes:

  • _compute_allowed_warehouse_ids now calls one _drims_allowed_warehouses helper on the incident. It is written to tolerate an empty incident and to run the warehouse search only on the fallback path, so a list view computing this field costs no extra query per row in the common case.
  • test_registrant.py comment fixed (draft, not alert); the cron's status list now includes draft.
  • Units/Distributed asymmetry: agreed it is fine, and I would rather record it in the ticket than in a code comment that will drift — noted on OP#1100.
  • Alert having no UI path in base spp_hazard: intended. Alert is a DRIMS concept; the state lives in the base module because the base state machine owns status, but the button belongs with the module that uses it.

spp_drims + spp_hazard suites: 359 tests, 0 failed on this branch. pre-commit clean, READMEs regenerated.

Merge order: this branch and #414 both touch spp_drims's manifest and changelog, so whichever goes second will conflict there — keep the higher version and concatenate the entries. #414 also carries the constraint that two of the deferred items above depend on, so I would land it first.

@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 are done and verified, the questions are answered credibly, and the fix quality matches the rest of this batch.

What I re-verified on f5592290 / c433ad82:

  • The migration is correct in the way that matters. It expires the drims_distributed_value / drims_stock_value cache rows before recomputing — checked against spp.data.value.read_values, which excludes expired rows, so the recompute cannot read the pre-change numbers straight back — then add_to_compute over all incidents including closed ones, which is exactly where a stale figure would otherwise sit forever. 19.0.3.1.0 matches the migration directory; spp_hazard's bump needs no migration of its own. Changelogs regenerated on both modules.
  • The lifecycle gate covers all four actions (action_close, action_set_active, action_set_recovery, and spp_drims's action_set_alert), with the four new tests pinning reopen-refused, re-close-refused, draft-deleted-not-closed, and the open lifecycle intact.
  • The write() posture is accepted, and for a better reason than cost: the stored KPI computes legitimately write to closed incidents — this PR's own migration does — so a blanket guard would break the mechanism that keeps closed incidents' numbers honest. Now stated in the PR body, which is what I asked for.
  • The #414 fixture collision is fixed on this side (c433ad82), with the right call on not yet dropping the duplicate donation guard — removing it before #414's constraint exists on this branch would leave test_1158_donation_blocked_when_closed asserting nothing.

On the deferred items — dispatch-after-closure guard, personnel incident_id re-pointing, and the request/donation creation asymmetry — the reasoning for deferring each to the #414/#433/#390 reconciliation is sound, and they're now tracked as a ticket on OpenProject so they survive the queue.

One item that crossed with your push by two minutes: the OP#1160 provenance ruling landed (Units/Products keeps the #1160 definition — your migration already matches it), with one suggestion attached — label the dashboard tiles (or their tooltips) so the provenance-vs-location split between Units/Products and Stock Value is legible. Fine as a follow-up.

Your merge-order input (land #414 first; whichever of the two goes second keeps the higher version and concatenates HISTORY) has been passed to Edwin for the queue decision. Merge itself stays with him per the usual flow.

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