Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
605 changes: 555 additions & 50 deletions spp_graduation/README.rst

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion spp_graduation/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
"maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"],
"depends": [
"base",
"spp_registry",
"spp_security",
"mail",
],
"external_dependencies": {"python": ["dateutil"]},
"external_dependencies": {"python": ["python-dateutil"]},
"data": [
"security/privileges.xml",
"security/graduation_security.xml",
Expand Down
12 changes: 6 additions & 6 deletions spp_graduation/data/graduation_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<field name="code">STANDARD</field>
<field name="sequence">10</field>
<field name="is_positive_exit" eval="True" />
<field name="is_requires_assessment" eval="True" />
<field name="is_requires_approval" eval="True" />
<field name="is_assessment_required" eval="True" />
<field name="is_approval_required" eval="True" />
<field name="post_graduation_monitoring_months">12</field>
<field
name="description"
Expand All @@ -20,8 +20,8 @@
<field name="code">EARLY</field>
<field name="sequence">20</field>
<field name="is_positive_exit" eval="True" />
<field name="is_requires_assessment" eval="True" />
<field name="is_requires_approval" eval="True" />
<field name="is_assessment_required" eval="True" />
<field name="is_approval_required" eval="True" />
<field name="post_graduation_monitoring_months">18</field>
<field
name="description"
Expand All @@ -34,8 +34,8 @@
<field name="code">ADMIN_EXIT</field>
<field name="sequence">30</field>
<field name="is_positive_exit" eval="False" />
<field name="is_requires_assessment" eval="False" />
<field name="is_requires_approval" eval="True" />
<field name="is_assessment_required" eval="False" />
<field name="is_approval_required" eval="True" />
<field name="post_graduation_monitoring_months">0</field>
<field
name="description"
Expand Down
60 changes: 43 additions & 17 deletions spp_graduation/models/graduation_assessment.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


class GraduationAssessment(models.Model):
_name = "spp.graduation.assessment"
_description = "Graduation Assessment"
_inherit = ["mail.thread", "mail.activity.mixin"]
_order = "assessment_date desc"
_check_company_auto = True

name = fields.Char(
compute="_compute_name",
Expand All @@ -17,11 +21,13 @@ class GraduationAssessment(models.Model):
string="Beneficiary",
required=True,
tracking=True,
domain=[("is_registrant", "=", True)],
)
pathway_id = fields.Many2one(
"spp.graduation.pathway",
required=True,
tracking=True,
check_company=True,
)

assessment_date = fields.Date(
Expand Down Expand Up @@ -91,7 +97,7 @@ class GraduationAssessment(models.Model):

company_id = fields.Many2one("res.company", default=lambda self: self.env.company)

@api.depends("partner_id", "pathway_id", "assessment_date")
@api.depends("partner_id", "pathway_id")
def _compute_name(self):
for rec in self:
if rec.partner_id and rec.pathway_id:
Expand Down Expand Up @@ -124,33 +130,43 @@ def _compute_scores(self):
def _compute_monitoring_end(self):
for rec in self:
if rec.graduation_date and rec.pathway_id.post_graduation_monitoring_months:
from dateutil.relativedelta import relativedelta

rec.monitoring_end_date = rec.graduation_date + relativedelta(
months=rec.pathway_id.post_graduation_monitoring_months
)
else:
rec.monitoring_end_date = False

def action_submit(self):
self.state = "submitted"
for rec in self:
if rec.state != "draft":
raise UserError(_("Only draft assessments can be submitted."))
rec.state = "submitted"

def action_approve(self):
self.write(
{
"state": "approved",
"approved_by_id": self.env.user.id,
"approved_date": fields.Datetime.now(),
}
)
if self.recommendation == "graduate":
self.graduation_date = fields.Date.today()
for rec in self:
if rec.state != "submitted":
raise UserError(_("Only submitted assessments can be approved."))
rec.write(
{
"state": "approved",
"approved_by_id": self.env.user.id,
"approved_date": fields.Datetime.now(),
}
)
if rec.recommendation == "graduate":
rec.graduation_date = fields.Date.today()

def action_reject(self):
self.state = "rejected"
for rec in self:
if rec.state != "submitted":
raise UserError(_("Only submitted assessments can be rejected."))
rec.state = "rejected"

def action_reset_draft(self):
self.state = "draft"
for rec in self:
if rec.state not in ("submitted", "rejected"):
raise UserError(_("Only submitted or rejected assessments can be reset to draft."))
rec.state = "draft"


class GraduationCriteriaResponse(models.Model):
Expand All @@ -174,4 +190,14 @@ class GraduationCriteriaResponse(models.Model):

value = fields.Char(help="Actual value observed")
notes = fields.Text()
evidence_attachment_ids = fields.Many2many("ir.attachment", string="Evidence Attachments")
evidence_attachment_ids = fields.Many2many(
"ir.attachment",
relation="spp_graduation_response_attachment_rel",
string="Evidence Attachments",
)

@api.constrains("score")
def _check_score_range(self):
for response in self:
if response.score < 0 or response.score > 1:
raise ValidationError(_("Score must be between 0 and 1. Got %(score)s.", score=response.score))
11 changes: 10 additions & 1 deletion spp_graduation/models/graduation_criteria.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from odoo import fields, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class GraduationCriteria(models.Model):
Expand Down Expand Up @@ -34,3 +35,11 @@ class GraduationCriteria(models.Model):
)

active = fields.Boolean(default=True)

@api.constrains("weight")
def _check_weight_positive(self):
for criteria in self:
if criteria.weight <= 0:
raise ValidationError(
_("Weight must be greater than zero for criteria '%(name)s'.", name=criteria.name)
)
6 changes: 3 additions & 3 deletions spp_graduation/models/graduation_pathway.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class GraduationPathway(models.Model):
_name = "spp.graduation.pathway"
_description = "Graduation Pathway"
_order = "sequence, name"
_check_company_auto = True

name = fields.Char(required=True)
code = fields.Char()
Expand All @@ -17,8 +18,8 @@ class GraduationPathway(models.Model):
help="Positive exit (graduation) vs negative exit (removed)",
)

is_requires_assessment = fields.Boolean(default=True)
is_requires_approval = fields.Boolean(default=True)
is_assessment_required = fields.Boolean(default=True)
is_approval_required = fields.Boolean(default=True)

criteria_ids = fields.One2many("spp.graduation.criteria", "pathway_id", string="Criteria")

Expand All @@ -30,7 +31,6 @@ class GraduationPathway(models.Model):
criteria_count = fields.Integer(
compute="_compute_criteria_count",
store=True,
default=0,
)

company_id = fields.Many2one("res.company", default=lambda self: self.env.company)
Expand Down
71 changes: 39 additions & 32 deletions spp_graduation/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,62 @@
Manages beneficiary graduation from time-bound social protection programs. Defines graduation pathways with weighted criteria, conducts assessments against those criteria, calculates readiness scores, and tracks graduation outcomes with post-graduation monitoring periods. Supports both positive exits (graduation) and negative exits (program removal).
Manages beneficiary graduation and exit from time-bound social protection programs. Defines
graduation pathways with weighted criteria, conducts assessments against those criteria, calculates
readiness scores, and tracks graduation outcomes with post-graduation monitoring periods. Supports
both positive exits (graduation) and negative exits (program removal).

### Key Capabilities

- Define graduation pathways with configurable criteria, exit type, and monitoring duration
- Create weighted criteria with different assessment methods (self-report, verification, computed, observation)
- Conduct beneficiary assessments with criteria responses and evidence attachments
- Calculate readiness scores based on weighted criteria and enforce required criteria
- Submit assessments for manager approval through a draft/submitted/approved/rejected workflow
- Track graduation dates and compute post-graduation monitoring periods
- Filter assessments by assessor, state, pathway, and recommendation
- Define graduation pathways with configurable criteria, exit type (`is_positive_exit`), and monitoring duration
- Create weighted criteria with four assessment methods: self-report, verification, computed, observation
- Conduct beneficiary assessments with per-criterion scores, a manual met/not-met judgment, and notes
- Calculate weighted readiness scores (0–1) from `score` fields and enforce required criteria via `is_met` flags through `_compute_scores()`. The `score` (numeric, 0–1) and `is_met` (boolean) fields serve different purposes: `score` drives the weighted readiness score, while `is_met` is a qualitative assessor judgment used to check whether required criteria are satisfied. They are intentionally independent because some assessment methods (e.g., field observation) may not map cleanly to a numeric score.
- Approve assessments through a draftsubmittedapproved/rejected workflow; approval auto-sets `graduation_date` when recommendation is "graduate"
- Compute `monitoring_end_date` from `graduation_date` + pathway's `post_graduation_monitoring_months`
- Ships with three pre-configured pathways: Standard Graduation (12 months monitoring), Early Graduation (18 months), and Administrative Exit (negative, 0 months)

### Key Models

| Model | Description |
| ---------------------------------- | -------------------------------------------------------- |
| `spp.graduation.pathway` | Defines a graduation pathway with criteria and exit type |
| `spp.graduation.criteria` | Individual criterion within a pathway with weight and method |
| `spp.graduation.assessment` | Assessment of a beneficiary against a pathway with scores |
| `spp.graduation.criteria.response` | Response to a specific criterion within an assessment |
| Model | Description |
| ---------------------------------- | ------------------------------------------------------------------------ |
| `spp.graduation.pathway` | Graduation pathway with exit type, approval/assessment flags, and criteria |
| `spp.graduation.criteria` | Weighted criterion within a pathway; has assessment method and required flag |
| `spp.graduation.assessment` | Assessment of a beneficiary against a pathway; tracks scores and approval state |
| `spp.graduation.criteria.response` | Per-criterion response with `score`, `is_met`, `value`, `notes`, and `evidence_attachment_ids` |

### Configuration

After installing:

1. Navigate to **Graduation > Configuration > Pathways**
2. Create graduation pathways specifying exit type (positive/negative) and monitoring months
3. Add criteria to each pathway with weight, assessment method, and required flag
4. Users can then create assessments under **Graduation > Assessments > All Assessments**
1. Navigate to **Graduation > Configuration > Pathways** (managers only)
2. Three default pathways are pre-installed; create additional ones as needed
3. On each pathway, set `is_positive_exit`, `is_assessment_required`, `is_approval_required`, and `post_graduation_monitoring_months`
4. Open the **Criteria** tab on the pathway form to add criteria with weight, assessment method, and required flag (inline editable list)
5. Users create assessments under **Graduation > Assessments > All Assessments**

### UI Location

- **Menu**: Graduation (top-level menu)
- **Assessments**: Graduation > Assessments > All Assessments / My Assessments
- **Configuration**: Graduation > Configuration > Pathways (managers only)
- **Views**: List, kanban (grouped by state), and form views with approval workflow
- **Pathway Form**: Criteria tab shows inline editable criteria list
- **Assessment Form**: Criteria Responses and Recommendation tabs
- **Top-level menu**: Graduation (visible to `group_spp_graduation_user` and above)
- **Graduation > Assessments > All Assessments**: List, kanban (grouped by state), form, graph, and pivot views
- **Graduation > Assessments > My Assessments**: Same views, pre-filtered to current user's assessments
- **Graduation > Configuration > Pathways**: List and form views (managers only)
- **Pathway form**: Two-column layout with a **Criteria** tab containing an inline editable list
- **Assessment form**: **Overview** tab (beneficiary, pathway, scores, dates), **Criteria Responses** tab (inline editable list with `criteria_id`, `score`, `is_met`, `value`, `notes`, `evidence_attachment_ids`), **Recommendation** tab (selection + notes), and **History** tab (audit metadata). Statusbar shows draft/submitted/approved. Alert banners for submitted and rejected states.
- **Assessment form buttons**: Submit (draft), Approve/Reject (submitted, managers only), Reset to Draft (submitted or rejected, managers only)

### Security

| Group | Access |
| ------------------------------------------ | --------------------------------------------------------- |
| `spp_graduation.group_spp_graduation_user` | Read pathways/criteria; create/edit own assessments (no delete) |
| `spp_graduation.group_spp_graduation_manager` | Full CRUD on all graduation data and configuration |
| Group | Access |
| --------------------------------------------- | ----------------------------------------------------------------------- |
| `spp_graduation.group_spp_graduation_user` | Read pathways/criteria; read/write/create own assessments (no delete); full CRUD on own criteria responses |
| `spp_graduation.group_spp_graduation_manager` | Full CRUD on all graduation models |

Record rules restrict users to assessments where `assessor_id = current user` and responses on those assessments. Managers have unrestricted access. Multi-company isolation rules apply to pathways and assessments.

### Extension Points

- Inherit `spp.graduation.assessment` and override `_compute_scores()` to customize readiness calculation
- Override `_compute_scores()` on `spp.graduation.assessment` to customize readiness calculation logic
- Override `_compute_monitoring_end()` to change how monitoring end dates are derived
- Inherit `spp.graduation.pathway` to add domain-specific pathway fields
- Extend approval workflow by inheriting assessment actions (`action_submit`, `action_approve`)

- Inherit assessment workflow actions (`action_submit`, `action_approve`, `action_reject`, `action_reset_draft`)
### Dependencies

`base`, `spp_security`, `mail`
`base`, `spp_registry`, `spp_security`, `mail`
Loading
Loading