fix(spp_case_base): clear is_current when an intervention plan is completed - #478
Conversation
gonzalesedwin1123
left a comment
There was a problem hiding this comment.
Expert review
Thanks for the fix — it's correct, minimal, and exactly what #458 asked for: folding "is_current": False into the existing write() means downstream overrides and tracking observe it, matching how action_create_revision already ends a plan's tenure. We verified it independently:
- Green:
./spp t spp_case_base→ 0 failed, 0 errors of 211 tests. - Red: with only the model file reverted to
origin/19.0, exactlytest_complete_clears_is_currentfails on theis_currentassertion — the test genuinely pins the bug. - All
is_current/current_plan_idconsumers survive the change (has_active_planalready filtered by state; the case-form gate readshas_active_plan; the ribbon/list column/"Current Plans" filter all become more correct), and no other module reads this model'sis_currentor overridesaction_complete. - Lint is clean on the changed files; nothing removed or weakened in tests.
Requesting changes for two things the repo requires of every code fix to a released module, plus a coverage tweak. If you'd rather not deal with the repo-specific mechanics, say so and we're happy to push these onto your branch — the code change itself is done.
Must fix
- Version bump + changelog + regenerated docs.
spp_case_baseshipped at19.0.2.0.0in release 2026.08, so this fix needs:__manifest__.py→19.0.2.0.1, areadme/HISTORY.mdfragment (OCA style, newest-first:### 19.0.2.0.1+ a- fix(case): …bullet), and regeneratedREADME.rst/static/description/index.html. Note the generated files are environment-sensitive — easiest is to push the fragment + bump and apply the diff CI's pre-commit job prints. - Data migration for released DBs. Existing deployments already hold
state='completed' AND is_current=truerows, and this fix only covers future completions — after upgrade those cases stay blocked exactly as #458 describes. Please addspp_case_base/migrations/19.0.2.0.1/post-migration.pyrunning the literal statement:(Keep it literal — no f-strings/format/identifier composition, or Semgrep/pylint-odoo flag it.)UPDATE spp_case_intervention_plan SET is_current = false WHERE state = 'completed' AND is_current = true;
Should fix
- Assert the reported symptom, not just the flag. #458's user-facing complaint is "a finished plan blocks marking a new plan current". After
plan.action_complete(), also create a second plan on the same case (it defaults tois_current=True) and assert it does not raise — that's the assertion an accidental revert of this fix would have to break at the constraint level. - Please fill in the "Unit tests executed by the author" section of the PR body — it still contains the template placeholder. (Our run above can serve as the evidence line if you re-run and get the same.)
Observations for maintainers (not blocking this PR — we'll file follow-ups)
action_completehas no state guard (unlikeaction_activate), so a draft plan can be completed over RPC; post-fix that mis-call also silently strips the case's current plan, with no way back sinceaction_reset_to_draftrefusescompleted.- The #458 dead-end partially relocates: complete plan A → new plan B becomes current → "Create Revision" on A (button visible for completed plans) copies with
is_current: Trueand hits the one-current-plan constraint with a message that names no conflicting plan. spp_case_demo/models/generate_cases.py:381-390manufactures exactly the #458 state (is_current: Truewithstaterandomly"completed"); the invariant "completed ⇒ not current" is enforced nowhere but this one method.is_currentlackstracking=Truewhilestatehas it, so a plan losing current status never reaches the chatter.
Nits (take or leave)
- Test placement: it sits above
test_create_plan, ahead of the file's create → workflow ordering; a natural home is besidetest_plan_approval_workflow, which already completes a plan. - The file's convention elsewhere exercises role users (
with_user(...)); a case-worker variant would also prove the write is permitted under the worker record rule. - Docstring convention in this file is "Test …".
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 19.0 #478 +/- ##
==========================================
+ Coverage 76.26% 76.88% +0.61%
==========================================
Files 662 703 +41
Lines 44225 45740 +1515
==========================================
+ Hits 33729 35166 +1437
- Misses 10496 10574 +78
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
I'll grab the completion of this one @LunarCapsule127 as we want to include this issue in the next release of OpenSPP2 |
…rent plan The code fix covers future completions only. Databases released at 19.0.2.0.0 still hold rows with state='completed' AND is_current=true, so current_plan_id keeps pointing at finished work and the one-current-plan constraint keeps refusing a successor plan -- the whole of OpenSPP#458 survives the upgrade for existing deployments. Bump to 19.0.2.0.1 and demote those rows in a post-migration. Only 'completed': action_create_revision already writes is_current=False alongside 'revised', so that state has no stale population. The log names the affected cases, because the remedy is per-case (promote the successor plan) and a bare count cannot be acted on.
…rom draft The is_current tests completed a plan straight out of draft, but the Complete button is invisible unless the plan is active, so they covered a transition no user can reach -- and a future state guard on action_complete would break them rather than the production path. Route them through submit -> approve -> activate first, and assert the released flag in test_plan_approval_workflow too, which already walks that path. Also pin the symptom OpenSPP#458 leads with rather than only the flag: a successor plan can be created and made current once the previous one completes. That assertion fails at the one-current-plan constraint if the fix is reverted, which is the level an accidental revert has to break.
… plan Two generator sites manufactured the state OpenSPP#458 is about. The close_case journey step wrote {"state": "completed"} directly, and _add_random_plan passed "completed" to create() alongside is_current=True -- so every generated demo database held plans that were finished yet still their case's current plan, with no actual_end_date and no way to start a successor plan. Both now complete through action_complete(), which stamps the end date and releases the flag. _add_random_plan keeps the same distribution of final states and adds its interventions before completing, so a completed demo plan still has a delivery record. No migration here: spp_case_base's 19.0.2.0.1 post-migration repairs the rows these sites already seeded, whichever module created them.
|
Thanks @gonzalesedwin1123 — taking you up on the offer to push the repo mechanics onto the branch, since we want this in next week's release. All four items are done, plus two of the nits and one of your maintainer observations. Details and evidence are in the updated PR body; summary against your list: Must fix
Should fix
Nits — took the test placement (now beside Maintainer observations — folded #3 (the demo generator) into this PR, because it's the one that survives the migration: a fresh demo install recreates the bug, so a release demo would still hit "cannot mark a new plan current". There were two sites, not one — Also cross-checked against a downstream implementation that carries a local override for this exact bug: the override becomes a no-op and can be deleted, the merged write passes its guard chain untouched, and the one real side-effect class for integrators is that
|
|
Follow-ups from the review are now filed, so nothing on the "deliberately left out" list is only living in this PR description:
Observation 3 (the demo generator) is fixed in this PR rather than deferred. #501 is ordered after #497 in its own text, since adding the guard changes the shape of the batching. |
| current_plan.sudo().write({"state": "completed"}) | ||
| # Through the action, not a bare state write: completing a | ||
| # plan also stamps actual_end_date and releases is_current. | ||
| current_plan.sudo().action_complete() |
| ) | ||
|
|
||
| if final_state == "completed": | ||
| plan.sudo().action_complete() |
gonzalesedwin1123
left a comment
There was a problem hiding this comment.
Re-review: approved
Thanks @kneckinator for picking this up, and @LunarCapsule127 for the original fix. All four items from the first review are addressed, and we re-verified independently on 2fcb709b rather than taking the PR body's word for it:
- Green:
./spp t spp_case_base→ 0 failed, 0 errors of 217;./spp t spp_case_demo→ 0 failed, 0 errors of 110. Matches the table in the body. - Red (model fix reverted): exactly
3 failed, 1 error—test_complete_clears_is_current,test_complete_by_case_worker,test_plan_approval_workflowon theis_currentassertion, andtest_complete_frees_the_current_plan_sloterroring withValidationError: Only one plan can be marked as current for a case.— the symptom-level assertion we asked for, failing at the constraint. - Red (demo generator reverted): exactly
2 failed, both new demo tests. - Migration is safe as raw SQL:
spp.case.current_plan_idandhas_active_planare bothstore=False, so there is no stored denormalisation left stale by bypassing the ORM. The statement is a single literal (Semgrep green), theRETURNING case_idlog is a genuine improvement over the bare count we asked for, and theif not versionguard is covered. - Release mechanics: both manifests bumped to
19.0.2.0.1, OCA-style### <version>fragments, and the regeneratedREADME.rst/index.htmlmatch CI byte-for-byte — CI's pre-commit job did run "Generate addons README files from fragments" and passed. Small correction to the note in your comment: pre-commit silently ignores themanual: truekey on that hook, so CI runs it in the default stage on every push; the diff you had to fish for with--hook-stage manualis the same one CI would have printed on failure. - No other open PR touches
spp_case_baseorspp_case_demo, so no version collision at merge time. Follow-ups #497–#501 are filed and open. - Both demo sites now reach
action_complete()with the plan inactive, so the demo stays compatible with anactive-only guard when #497 lands.
Optional (not blocking, take or leave)
test_migration_complete_clears_is_current.pylets the script'sWARNINGthrough to the test log (twice per run). Wrapping the two demoting tests inself.assertLogs("spp_case_base_post_migration_19_0_2_0_1", level="WARNING")and asserting the case id appears in the message would both keep test output clean and pin the per-case log line you added deliberately;assertNoLogson the no-op tests would pin the other half.
For the release notes (maintainer to-do, not this PR)
The downstream cross-check point is the one worth carrying into the 2026.09 notes: any integrator domain using plan_id.is_current as "the plan that matters" now excludes completed plans, and current_plan_id is empty after a normal completion. We'll add it when the notes are drafted.
Why is this change needed?
action_completemarked a planstate = "completed"and stampedactual_end_date, but never releasedis_current. Since the only other writer ofis_current = Falsewas the revision path, a plan that finished normally stayed the case's current plan indefinitely.That left every consumer of the pair reading an incoherent state:
current_plan_id(derived purely fromis_current) kept pointing at completed work, while "has an active plan" derivations read False. It also tripped the one-current-plan-per-case constraint, blocking anyone from marking a fresh plan as current.Fixes #458.
How was the change implemented?
spp_case_base— the fix. Folded"is_current": Falseinto the existingaction_completewrite, so completing a plan ends its tenure as the case's current plan the same wayaction_create_revisionalready does. The change goes throughwrite(), so downstream overrides observe it.spp_case_base— released databases. The code fix covers future completions only. Databases released at19.0.2.0.0still holdstate = 'completed' AND is_current = truerows, so after upgrading, those cases keep the whole of #458: a stalecurrent_plan_idand a constraint that still refuses a successor plan. Bumped to19.0.2.0.1and addedmigrations/19.0.2.0.1/post-migration.py, which demotes exactly those rows. Deliberately narrow — onlycompleted;action_create_revisionalready writesis_current = Falsealongsiderevised, so that state has no stale population. The log names the affectedspp.caseids, because the remedy is per-case (promote the successor plan) and a bare count cannot be acted on.spp_case_demo— stop re-creating the bug. Two generator sites manufactured the same state, so every generated demo database reproduced #458 on a fresh install, where no migration can help:_process_case_journey'sclose_casestep wrote{"state": "completed"}directly, bypassingaction_complete— the plan keptis_currentand never got anactual_end_date._add_random_planpassedstate = "completed"tocreate()alongsideis_current: True.Both now complete through
action_complete()._add_random_plankeeps the same distribution of final states and adds its interventions before completing, so a completed demo plan still has a delivery record. No migration in this module —spp_case_base's post-migration repairs the rows these sites already seeded, whichever module created them.New unit tests
spp_case_base/tests/test_case_intervention_plan.pytest_complete_clears_is_current— an active plan is the case'scurrent_plan_id; afteraction_complete(),stateiscompleted,is_currentis False, and the case reports neither a current plan nor an active one.test_complete_frees_the_current_plan_slot— the symptom spp_case_base: action_complete never clears is_current, leaving a finished plan as the case's current plan #458 leads with: a successor plan can be created and becomes current once the previous plan completes. Without the fix this fails at the one-current-plan constraint, which is the level an accidental revert has to break.test_complete_by_case_worker— the assigned case worker may complete a plan and release the flag, so the write is permitted under the worker record rule (own cases only), not just as superuser.test_plan_approval_workflow(existing) now also asserts the released flag, covering the fix on the genuine draft → approved → active → completed path.All three new tests reach
activethroughsubmit → approve → activatefirst, via a_active_planhelper. Completing straight fromdraftcovered a transition the UI cannot produce (the Complete button isinvisible="state != 'active'"), and would have broken if a state guard were later added toaction_completeinstead of the production path being covered.spp_case_base/tests/test_migration_complete_clears_is_current.py(new file) — loads the script throughimportlib, matchingspp_gis/tests/test_migration_geofence_tags.py:test_migration_demotes_completed_plans— releasesis_current, leavesstateandactual_end_dateuntouched.test_migration_frees_the_current_plan_slot— a successor plan can be created after the script runs.test_migration_leaves_unfinished_plans_alone— anactivecurrent plan stays current.test_migration_skips_fresh_install—migrate(cr, None)returns early.spp_case_demo/tests/test_generate_cases.pytest_journey_close_case_releases_the_current_plan— after theclose_casestep the plan iscompleted, has anactual_end_date, is notis_current, and the case reports no current plan.test_add_random_plan_completed_plan_is_not_current— forces the random state draw tocompletedand asserts the same invariant, plus that interventions exist on the finished plan.Unit tests executed by the author
./spp t <module>, Docker mode, on2fcb709b:spp_case_basespp_case_demospp_case_celspp_case_registryspp_case_sessionNo tests removed;
spp_case_basegoes 211 → 217 andspp_case_demo108 → 110.Red/green checks (each test confirmed non-vacuous by reverting the thing it pins):
3 failed, 1 error:test_complete_clears_is_current,test_complete_by_case_workerandtest_plan_approval_workflowfail on theis_currentassertion;test_complete_frees_the_current_plan_sloterrors withValidationError: Only one plan can be marked as current for a case.The migration tests correctly stay green — they do not depend onaction_complete.completed→revised) →1 failed, 1 error, both in the migration test; the two non-mutation assertions correctly stay green.2 failed, both new demo tests.Migration exercised end to end, not only unit-tested. Installed
spp_case_baseat19.0.2.0.0on a real database, seeded three plans by SQL — twocompleted+is_current(one withactual_end_date, one without) and oneactive+is_currentcontrol — then bumped the manifest and ran./spp update:Both stale rows flipped to
is_current = false, theactivecontrol row was untouched, and on the upgraded databasecurrent_plan_idwas empty,has_active_planFalse, and a successor plan could be created as current.Lint:
pre-commit cleanthenpre-commit run --files <changed files>— all hooks pass.semgrepcannot run in this environment (TypeError: Metaclasses with custom tp_new are not supportedfrom its protobuf dependency on Python 3.14), so CI is the check for it; the migration's SQL is a single string literal with no%,.format(), f-string or+concatenation, and the change adds nosudo()call.README.rst/static/description/index.htmlregenerated for both modules with the pinned generator deps, and the incidental drift the generator produces in six unrelated modules was reverted.How to test manually
spp_case_baseand confirm the log line above, that the plan is no longer current, and that a successor plan can be created.spp_case_demo) with plans and closures enabled, then check that no plan is bothcompletedandIs Current Plan— the Intervention Plans list shows both columns.Related links
Notes for reviewers
Taking this over from @LunarCapsule127 so it makes the next release — the one-line model fix and its first test are theirs. The commits above add the release mechanics (version bump, changelog, regenerated docs), the data migration for released databases, the demo-data fix, and the test hardening @gonzalesedwin1123 asked for.
Downstream cross-check. Verified against a downstream case-management module built on
spp_case_basethat carries a localaction_completeoverride for this exact bug. Findings:is_currentaftersuper();filtered("is_current")is then empty), so it can be deleted with no behaviour change.write()gates a locked set of content fields (goals,expected_outcomes,client_responsibilities,start_date,target_end_date) and its state-tier gate inspects only the targetstate.is_currentis in neither, so folding it into the samewrite()asstatebehaves exactly like the separate write it replaces.is_currentas a proxy for "the plan that matters" now excludes completed plans. That downstream module hit precisely this in a lapse cron whose candidate domain requiredplan_id.is_current, and had to widen it to"|", ("plan_id.is_current", "=", True), ("plan_id.state", "=", "completed"). Worth a grep foris_currentin domains before upgrading.current_plan_idand had to switch to "the case has no plan at all", since a normally-completed plan now leavescurrent_plan_idempty.Deliberately left out of this PR
Found while reviewing; each is pre-existing and none is introduced by this change. Filed as follow-ups rather than grown into a release-week fix:
action_completehas no state guard. Unlikeaction_activate(and unlikespp.case.assessment.action_complete, which does guard), it accepts any state, so an RPC call can complete adraftplan — and now also strip the case's current-plan pointer, withaction_reset_to_draftrefusingcompletedso there is no way back. The tests here already drivesubmit → approve → activate, so they pass anactive-only guard unchanged.action_create_revisionignores who holdsis_current. Complete A → create B (now current) → "Create Revision" on A (the button is visible forcompleted) copies withis_current: Trueand trips the constraint, naming no conflicting plan. The transaction rolls back cleanly, so there is no partial state, but the dead-end spp_case_base: action_complete never clears is_current, leaving a finished plan as the case's current plan #458 removed partly relocates here. Separately and independent of this PR: revising acompletedplan overwritesstatewithrevisedwhile leavingactual_end_dateset, erasing the completion record.is_currentagainststate. The checkbox is editable on the plan form, so a user can re-tick it on a completed plan and recreate the incoherent pair; the constraint only rejects a second current plan. This is the root fix, and explicitly not a release-week change: the constraint as written would rejectcreate({"state": "completed", ...}), which existing tests here and downstream fixtures rely on. The issue also covers the altitude option —is_currentis a hand-maintained denormalized flag with three writers and a Python-only, un-sudo'd constraint; making it derived and backing it with a partial unique index would remove this bug class rather than its third instance.is_currentlackstracking=Truewhilestatehas it, so a plan losing current status never reaches the chatter.action_completeandaction_approvewrite per record although every value is uniform across the recordset. Left as is here to match the method directly above; ordered after spp_case_base: action_complete has no state guard — an RPC call can complete a draft plan and strand the case with no current plan #497, since adding the guard changes the natural shape.@gonzalesedwin1123's third maintainer observation — the demo generator — is fixed in this PR rather than deferred, since it is the one that survives the migration: a fresh demo install would otherwise recreate the bug.