Filed at origin/19.0 (f6213293). From the review of #478 (fixes #458) — did not block that PR; @gonzalesedwin1123 raised the same point in his review there.
Summary
spp.case.intervention.plan.action_complete (spp_case_base/models/case_intervention_plan.py:191) accepts a plan in any state:
def action_complete(self):
"""Mark plan as completed."""
for plan in self:
plan.write({
"state": "completed",
"actual_end_date": fields.Date.context_today(self),
"is_current": False, # added by #478
})
return True
Every neighbour guards. action_activate two methods up refuses a non-approved plan (:186-187), action_reset_to_draft refuses completed/revised (:239-240), and the sibling model's spp.case.assessment.action_complete raises UserError(_("Only draft assessments can be completed.")) (spp_case_base/models/case_assessment.py:193-194). The plan's Complete button is gated invisible="state != 'active'" (views/case_intervention_views.xml:33-37), so the view already encodes the intended precondition — it just isn't enforced server-side.
The model grants write to group_case_worker, group_case_supervisor and group_case_manager (security/ir.model.access.csv:18-20), so the call is reachable over RPC by any case worker on their own cases.
Impact
Worse after #478 than before it, which is why it is worth fixing now rather than later.
Pre-#478, completing a draft plan produced a wrong state and a spurious actual_end_date. Post-#478 the same call also clears is_current, and there is no supported way back: action_reset_to_draft refuses a completed plan (:239-240), so recovering the case's current plan means hand-ticking the Is Current Plan checkbox on a completed record — which is exactly the incoherent pair #458 exists to eliminate, and which #478's data migration exists to clean up.
So a single mis-aimed RPC call (or a scheduled action, or an action_complete() in a data script) can strand a case with no current plan and no route to restore one through the UI.
Suggested fix
Mirror the view's own gate and the module's convention:
def action_complete(self):
"""Mark plan as completed."""
for plan in self:
if plan.state != "active":
raise ValidationError(_("Only active plans can be completed."))
plan.write({...})
ValidationError rather than UserError to match action_activate directly above it (the assessment sibling uses UserError; the plan model is internally consistent on ValidationError).
Worth checking before merging: spp_case_demo's generator calls action_complete() on plans it has created as active (spp_case_demo/models/generate_cases.py, the close_case journey step and _add_random_plan — both routed through the action by #478), so the demo path already satisfies an active-only guard. A grep for other action_complete() callers is a one-line check.
Tests
Filed at
origin/19.0(f6213293). From the review of #478 (fixes #458) — did not block that PR; @gonzalesedwin1123 raised the same point in his review there.Summary
spp.case.intervention.plan.action_complete(spp_case_base/models/case_intervention_plan.py:191) accepts a plan in any state:Every neighbour guards.
action_activatetwo methods up refuses a non-approvedplan (:186-187),action_reset_to_draftrefusescompleted/revised(:239-240), and the sibling model'sspp.case.assessment.action_completeraisesUserError(_("Only draft assessments can be completed."))(spp_case_base/models/case_assessment.py:193-194). The plan's Complete button is gatedinvisible="state != 'active'"(views/case_intervention_views.xml:33-37), so the view already encodes the intended precondition — it just isn't enforced server-side.The model grants write to
group_case_worker,group_case_supervisorandgroup_case_manager(security/ir.model.access.csv:18-20), so the call is reachable over RPC by any case worker on their own cases.Impact
Worse after #478 than before it, which is why it is worth fixing now rather than later.
Pre-#478, completing a
draftplan produced a wrongstateand a spuriousactual_end_date. Post-#478 the same call also clearsis_current, and there is no supported way back:action_reset_to_draftrefuses acompletedplan (:239-240), so recovering the case's current plan means hand-ticking theIs Current Plancheckbox on a completed record — which is exactly the incoherent pair #458 exists to eliminate, and which #478's data migration exists to clean up.So a single mis-aimed RPC call (or a scheduled action, or an
action_complete()in a data script) can strand a case with no current plan and no route to restore one through the UI.Suggested fix
Mirror the view's own gate and the module's convention:
ValidationErrorrather thanUserErrorto matchaction_activatedirectly above it (the assessment sibling usesUserError; the plan model is internally consistent onValidationError).Worth checking before merging:
spp_case_demo's generator callsaction_complete()on plans it has created asactive(spp_case_demo/models/generate_cases.py, theclose_casejourney step and_add_random_plan— both routed through the action by #478), so the demo path already satisfies anactive-only guard. A grep for otheraction_complete()callers is a one-line check.Tests
action_complete()on adraftplan raises, and the plan keepsis_current.action_complete()on apending_approvaland anapprovedplan raises.test_plan_approval_workflowand the threetest_complete_*tests added by fix(spp_case_base): clear is_current when an intervention plan is completed #478 already drivesubmit → approve → activatebefore completing, so they pass anactive-only guard unchanged — that ordering was chosen in fix(spp_case_base): clear is_current when an intervention plan is completed #478 partly in anticipation of this.