Skip to content

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

Description

@kneckinator

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions