Skip to content

Commit 6851b6a

Browse files
Merge pull request #96 from OpenSPP/fix/spp-graduation-readme
fix(spp_graduation): improve code quality, UX, tests, and documentation
2 parents 93fa4cb + 19ab7ea commit 6851b6a

15 files changed

Lines changed: 2329 additions & 314 deletions

spp_graduation/README.rst

Lines changed: 557 additions & 52 deletions
Large diffs are not rendered by default.

spp_graduation/__manifest__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
"author": "OpenSPP.org",
88
"website": "https://github.com/OpenSPP/OpenSPP2",
99
"license": "LGPL-3",
10-
"development_status": "Beta",
10+
"development_status": "Production/Stable",
1111
"maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"],
1212
"depends": [
1313
"base",
14+
"spp_registry",
1415
"spp_security",
1516
"mail",
1617
],
17-
"external_dependencies": {"python": ["dateutil"]},
18+
"external_dependencies": {"python": ["python-dateutil"]},
1819
"data": [
1920
"security/privileges.xml",
2021
"security/graduation_security.xml",

spp_graduation/data/graduation_data.xml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<field name="code">STANDARD</field>
77
<field name="sequence">10</field>
88
<field name="is_positive_exit" eval="True" />
9-
<field name="is_requires_assessment" eval="True" />
10-
<field name="is_requires_approval" eval="True" />
9+
<field name="is_assessment_required" eval="True" />
10+
<field name="is_approval_required" eval="True" />
1111
<field name="post_graduation_monitoring_months">12</field>
1212
<field
1313
name="description"
@@ -20,8 +20,8 @@
2020
<field name="code">EARLY</field>
2121
<field name="sequence">20</field>
2222
<field name="is_positive_exit" eval="True" />
23-
<field name="is_requires_assessment" eval="True" />
24-
<field name="is_requires_approval" eval="True" />
23+
<field name="is_assessment_required" eval="True" />
24+
<field name="is_approval_required" eval="True" />
2525
<field name="post_graduation_monitoring_months">18</field>
2626
<field
2727
name="description"
@@ -34,8 +34,8 @@
3434
<field name="code">ADMIN_EXIT</field>
3535
<field name="sequence">30</field>
3636
<field name="is_positive_exit" eval="False" />
37-
<field name="is_requires_assessment" eval="False" />
38-
<field name="is_requires_approval" eval="True" />
37+
<field name="is_assessment_required" eval="False" />
38+
<field name="is_approval_required" eval="True" />
3939
<field name="post_graduation_monitoring_months">0</field>
4040
<field
4141
name="description"

spp_graduation/models/graduation_assessment.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
from odoo import api, fields, models
1+
from dateutil.relativedelta import relativedelta
2+
3+
from odoo import _, api, fields, models
4+
from odoo.exceptions import UserError, ValidationError
25

36

47
class GraduationAssessment(models.Model):
58
_name = "spp.graduation.assessment"
69
_description = "Graduation Assessment"
710
_inherit = ["mail.thread", "mail.activity.mixin"]
811
_order = "assessment_date desc"
12+
_check_company_auto = True
913

1014
name = fields.Char(
1115
compute="_compute_name",
@@ -17,11 +21,13 @@ class GraduationAssessment(models.Model):
1721
string="Beneficiary",
1822
required=True,
1923
tracking=True,
24+
domain=[("is_registrant", "=", True)],
2025
)
2126
pathway_id = fields.Many2one(
2227
"spp.graduation.pathway",
2328
required=True,
2429
tracking=True,
30+
check_company=True,
2531
)
2632

2733
assessment_date = fields.Date(
@@ -91,7 +97,7 @@ class GraduationAssessment(models.Model):
9197

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

94-
@api.depends("partner_id", "pathway_id", "assessment_date")
100+
@api.depends("partner_id", "pathway_id")
95101
def _compute_name(self):
96102
for rec in self:
97103
if rec.partner_id and rec.pathway_id:
@@ -124,33 +130,43 @@ def _compute_scores(self):
124130
def _compute_monitoring_end(self):
125131
for rec in self:
126132
if rec.graduation_date and rec.pathway_id.post_graduation_monitoring_months:
127-
from dateutil.relativedelta import relativedelta
128-
129133
rec.monitoring_end_date = rec.graduation_date + relativedelta(
130134
months=rec.pathway_id.post_graduation_monitoring_months
131135
)
132136
else:
133137
rec.monitoring_end_date = False
134138

135139
def action_submit(self):
136-
self.state = "submitted"
140+
for rec in self:
141+
if rec.state != "draft":
142+
raise UserError(_("Only draft assessments can be submitted."))
143+
rec.state = "submitted"
137144

138145
def action_approve(self):
139-
self.write(
140-
{
141-
"state": "approved",
142-
"approved_by_id": self.env.user.id,
143-
"approved_date": fields.Datetime.now(),
144-
}
145-
)
146-
if self.recommendation == "graduate":
147-
self.graduation_date = fields.Date.today()
146+
for rec in self:
147+
if rec.state != "submitted":
148+
raise UserError(_("Only submitted assessments can be approved."))
149+
rec.write(
150+
{
151+
"state": "approved",
152+
"approved_by_id": self.env.user.id,
153+
"approved_date": fields.Datetime.now(),
154+
}
155+
)
156+
if rec.recommendation == "graduate":
157+
rec.graduation_date = fields.Date.today()
148158

149159
def action_reject(self):
150-
self.state = "rejected"
160+
for rec in self:
161+
if rec.state != "submitted":
162+
raise UserError(_("Only submitted assessments can be rejected."))
163+
rec.state = "rejected"
151164

152165
def action_reset_draft(self):
153-
self.state = "draft"
166+
for rec in self:
167+
if rec.state not in ("submitted", "rejected"):
168+
raise UserError(_("Only submitted or rejected assessments can be reset to draft."))
169+
rec.state = "draft"
154170

155171

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

175191
value = fields.Char(help="Actual value observed")
176192
notes = fields.Text()
177-
evidence_attachment_ids = fields.Many2many("ir.attachment", string="Evidence Attachments")
193+
evidence_attachment_ids = fields.Many2many(
194+
"ir.attachment",
195+
relation="spp_graduation_response_attachment_rel",
196+
string="Evidence Attachments",
197+
)
198+
199+
@api.constrains("score")
200+
def _check_score_range(self):
201+
for response in self:
202+
if response.score < 0 or response.score > 1:
203+
raise ValidationError(_("Score must be between 0 and 1. Got %(score)s.", score=response.score))

spp_graduation/models/graduation_criteria.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from odoo import fields, models
1+
from odoo import _, api, fields, models
2+
from odoo.exceptions import ValidationError
23

34

45
class GraduationCriteria(models.Model):
@@ -34,3 +35,11 @@ class GraduationCriteria(models.Model):
3435
)
3536

3637
active = fields.Boolean(default=True)
38+
39+
@api.constrains("weight")
40+
def _check_weight_positive(self):
41+
for criteria in self:
42+
if criteria.weight <= 0:
43+
raise ValidationError(
44+
_("Weight must be greater than zero for criteria '%(name)s'.", name=criteria.name)
45+
)

spp_graduation/models/graduation_pathway.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class GraduationPathway(models.Model):
55
_name = "spp.graduation.pathway"
66
_description = "Graduation Pathway"
77
_order = "sequence, name"
8+
_check_company_auto = True
89

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

20-
is_requires_assessment = fields.Boolean(default=True)
21-
is_requires_approval = fields.Boolean(default=True)
21+
is_assessment_required = fields.Boolean(default=True)
22+
is_approval_required = fields.Boolean(default=True)
2223

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

@@ -30,7 +31,6 @@ class GraduationPathway(models.Model):
3031
criteria_count = fields.Integer(
3132
compute="_compute_criteria_count",
3233
store=True,
33-
default=0,
3434
)
3535

3636
company_id = fields.Many2one("res.company", default=lambda self: self.env.company)
Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,62 @@
1-
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).
1+
Manages beneficiary graduation and exit from time-bound social protection programs. Defines
2+
graduation pathways with weighted criteria, conducts assessments against those criteria, calculates
3+
readiness scores, and tracks graduation outcomes with post-graduation monitoring periods. Supports
4+
both positive exits (graduation) and negative exits (program removal).
25

36
### Key Capabilities
47

5-
- Define graduation pathways with configurable criteria, exit type, and monitoring duration
6-
- Create weighted criteria with different assessment methods (self-report, verification, computed, observation)
7-
- Conduct beneficiary assessments with criteria responses and evidence attachments
8-
- Calculate readiness scores based on weighted criteria and enforce required criteria
9-
- Submit assessments for manager approval through a draft/submitted/approved/rejected workflow
10-
- Track graduation dates and compute post-graduation monitoring periods
11-
- Filter assessments by assessor, state, pathway, and recommendation
8+
- Define graduation pathways with configurable criteria, exit type (`is_positive_exit`), and monitoring duration
9+
- Create weighted criteria with four assessment methods: self-report, verification, computed, observation
10+
- Conduct beneficiary assessments with per-criterion scores, a manual met/not-met judgment, and notes
11+
- 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.
12+
- Approve assessments through a draftsubmittedapproved/rejected workflow; approval auto-sets `graduation_date` when recommendation is "graduate"
13+
- Compute `monitoring_end_date` from `graduation_date` + pathway's `post_graduation_monitoring_months`
14+
- Ships with three pre-configured pathways: Standard Graduation (12 months monitoring), Early Graduation (18 months), and Administrative Exit (negative, 0 months)
1215

1316
### Key Models
1417

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

2225
### Configuration
2326

2427
After installing:
2528

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

3135
### UI Location
3236

33-
- **Menu**: Graduation (top-level menu)
34-
- **Assessments**: Graduation > Assessments > All Assessments / My Assessments
35-
- **Configuration**: Graduation > Configuration > Pathways (managers only)
36-
- **Views**: List, kanban (grouped by state), and form views with approval workflow
37-
- **Pathway Form**: Criteria tab shows inline editable criteria list
38-
- **Assessment Form**: Criteria Responses and Recommendation tabs
37+
- **Top-level menu**: Graduation (visible to `group_spp_graduation_user` and above)
38+
- **Graduation > Assessments > All Assessments**: List, kanban (grouped by state), form, graph, and pivot views
39+
- **Graduation > Assessments > My Assessments**: Same views, pre-filtered to current user's assessments
40+
- **Graduation > Configuration > Pathways**: List and form views (managers only)
41+
- **Pathway form**: Two-column layout with a **Criteria** tab containing an inline editable list
42+
- **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.
43+
- **Assessment form buttons**: Submit (draft), Approve/Reject (submitted, managers only), Reset to Draft (submitted or rejected, managers only)
3944

4045
### Security
4146

42-
| Group | Access |
43-
| ------------------------------------------ | --------------------------------------------------------- |
44-
| `spp_graduation.group_spp_graduation_user` | Read pathways/criteria; create/edit own assessments (no delete) |
45-
| `spp_graduation.group_spp_graduation_manager` | Full CRUD on all graduation data and configuration |
47+
| Group | Access |
48+
| --------------------------------------------- | ----------------------------------------------------------------------- |
49+
| `spp_graduation.group_spp_graduation_user` | Read pathways/criteria; read/write/create own assessments (no delete); full CRUD on own criteria responses |
50+
| `spp_graduation.group_spp_graduation_manager` | Full CRUD on all graduation models |
51+
52+
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.
4653

4754
### Extension Points
4855

49-
- Inherit `spp.graduation.assessment` and override `_compute_scores()` to customize readiness calculation
56+
- Override `_compute_scores()` on `spp.graduation.assessment` to customize readiness calculation logic
57+
- Override `_compute_monitoring_end()` to change how monitoring end dates are derived
5058
- Inherit `spp.graduation.pathway` to add domain-specific pathway fields
51-
- Extend approval workflow by inheriting assessment actions (`action_submit`, `action_approve`)
52-
59+
- Inherit assessment workflow actions (`action_submit`, `action_approve`, `action_reject`, `action_reset_draft`)
5360
### Dependencies
5461

55-
`base`, `spp_security`, `mail`
62+
`base`, `spp_registry`, `spp_security`, `mail`

0 commit comments

Comments
 (0)