From 33b1488f0f9828b9b8a9d4c2455782d19c691e62 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 3 Jun 2026 09:51:18 +0800 Subject: [PATCH 01/16] feat(disability_registry): improve assistive device management (#1052) - Show Devices smart button even at zero count - Remove has_disability prerequisite to add/view assistive devices - Make status editable inline (dropdown) with row-level color; surface provision date and provider when status is Provided - Drop inaccurate DCI CD.DR.04 / ISO 9999 claims from device vocabulary - Reword assistive-device readme bullet (status, not status workflow) --- spp_disability_registry/README.rst | 3 +-- .../data/vocabulary_device.xml | 5 +--- spp_disability_registry/readme/DESCRIPTION.md | 2 +- .../static/description/index.html | 3 +-- .../views/registrant_views.xml | 24 +++++++++++-------- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/spp_disability_registry/README.rst b/spp_disability_registry/README.rst index 408cb994e..d6ff6d50a 100644 --- a/spp_disability_registry/README.rst +++ b/spp_disability_registry/README.rst @@ -40,8 +40,7 @@ Key Features scheduling - Impairment type, cause, and severity classifications using DCI vocabularies -- Assistive device management with status workflow (needed, requested, - provided) +- Assistive device management with status (needed, requested, provided) - Proxy response tracking for children - CEL function integration for program eligibility targeting diff --git a/spp_disability_registry/data/vocabulary_device.xml b/spp_disability_registry/data/vocabulary_device.xml index c1e355af0..c240f8f7d 100644 --- a/spp_disability_registry/data/vocabulary_device.xml +++ b/spp_disability_registry/data/vocabulary_device.xml @@ -2,15 +2,12 @@ Assistive Device Type urn:dci:cd:dr:04 2024 - Types of assistive devices aligned with ISO 9999 classification + Types of assistive devices True disability diff --git a/spp_disability_registry/readme/DESCRIPTION.md b/spp_disability_registry/readme/DESCRIPTION.md index 4e209323f..3ff8a72f5 100644 --- a/spp_disability_registry/readme/DESCRIPTION.md +++ b/spp_disability_registry/readme/DESCRIPTION.md @@ -7,6 +7,6 @@ Comprehensive disability assessment and registry management for social protectio - Disability indicator computation per WG standard (any domain with "a lot of difficulty" or "cannot do at all") - Review category system (MIE/MIP/MINE) with automatic next-review-date scheduling - Impairment type, cause, and severity classifications using DCI vocabularies -- Assistive device management with status workflow (needed, requested, provided) +- Assistive device management with status (needed, requested, provided) - Proxy response tracking for children - CEL function integration for program eligibility targeting diff --git a/spp_disability_registry/static/description/index.html b/spp_disability_registry/static/description/index.html index 27db6080d..b2538da1f 100644 --- a/spp_disability_registry/static/description/index.html +++ b/spp_disability_registry/static/description/index.html @@ -387,8 +387,7 @@

Key Features

scheduling
  • Impairment type, cause, and severity classifications using DCI vocabularies
  • -
  • Assistive device management with status workflow (needed, requested, -provided)
  • +
  • Assistive device management with status (needed, requested, provided)
  • Proxy response tracking for children
  • CEL function integration for program eligibility targeting
  • diff --git a/spp_disability_registry/views/registrant_views.xml b/spp_disability_registry/views/registrant_views.xml index 2f7a7ddbf..12c8d611c 100644 --- a/spp_disability_registry/views/registrant_views.xml +++ b/spp_disability_registry/views/registrant_views.xml @@ -51,7 +51,7 @@ type="object" class="oe_stat_button" icon="fa-medkit" - invisible="not is_registrant or is_group or assistive_device_count == 0" + invisible="not is_registrant or is_group" > - -
    + +
    - + + From e4eb1bd00be54cc9d396ac6e9a5d8255f2b39a1e Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 3 Jun 2026 10:35:28 +0800 Subject: [PATCH 02/16] feat(disability_registry): age-driven assessment type with manual override (#1050) - Add Disability Registry settings with an 'Allow manual assessment type' toggle (res.config.settings + Configuration > Settings menu) - Default: assessment type is readonly and auto-determined from the registrant's age; a date of birth is required (validation) - When the toggle is enabled: assessment type is a manual dropdown and a date of birth is no longer required - Rename the 'WG Assessment' tab to 'WG/CFM Assessment' and show the WG-SS questionnaire only for the adult type; add a placeholder for the CFM forms (added by #1048 / #1049) --- spp_disability_registry/__manifest__.py | 1 + spp_disability_registry/models/__init__.py | 1 + spp_disability_registry/models/assessment.py | 63 +++++++++++++--- .../models/res_config_settings.py | 15 ++++ .../views/assessment_views.xml | 72 ++++++++++++------- spp_disability_registry/views/menus.xml | 9 +++ .../views/res_config_settings_views.xml | 36 ++++++++++ 7 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 spp_disability_registry/models/res_config_settings.py create mode 100644 spp_disability_registry/views/res_config_settings_views.xml diff --git a/spp_disability_registry/__manifest__.py b/spp_disability_registry/__manifest__.py index ea1779c54..9b83cc3ad 100644 --- a/spp_disability_registry/__manifest__.py +++ b/spp_disability_registry/__manifest__.py @@ -31,6 +31,7 @@ "views/assessment_views.xml", "views/assistive_device_views.xml", "views/registrant_views.xml", + "views/res_config_settings_views.xml", "views/menus.xml", ], "demo": [ diff --git a/spp_disability_registry/models/__init__.py b/spp_disability_registry/models/__init__.py index dd404f708..6578c1071 100644 --- a/spp_disability_registry/models/__init__.py +++ b/spp_disability_registry/models/__init__.py @@ -2,3 +2,4 @@ from . import assistive_device from . import cel_disability_functions from . import registrant +from . import res_config_settings diff --git a/spp_disability_registry/models/assessment.py b/spp_disability_registry/models/assessment.py index ff117d8e9..f2edc810a 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -77,6 +77,13 @@ class SppDisabilityAssessment(models.Model): compute="_compute_age_at_assessment", store=True, ) + age_restriction_enforced = fields.Boolean( + string="Age Restriction Enforced", + compute="_compute_age_restriction_enforced", + help="Technical flag driving the form: True when the assessment type is " + "auto-determined by age (default). Toggled by the 'Allow manual assessment " + "type' setting in Disability Registry configuration.", + ) # === WG-SS Responses (6 domains) === wg_seeing = fields.Selection( @@ -227,18 +234,56 @@ def _compute_age_at_assessment(self): delta = relativedelta(rec.assessment_date, rec.registrant_id.birthdate) rec.age_at_assessment = delta.years + @api.model + def _disability_disregard_age(self): + """Read the 'disregard age for assessment type' configuration flag.""" + # nosemgrep: odoo-sudo-without-context — standard Odoo pattern for system parameter access + icp = self.env["ir.config_parameter"].sudo() + return icp.get_param("spp_disability_registry.disregard_age", "False") == "True" + + def _assessment_type_for_age(self): + """Return the WG/CFM assessment type implied by the age at assessment.""" + self.ensure_one() + age = self.age_at_assessment + if age >= 18: + return "wg_ss" + elif age >= 5: + return "cfm_5_17" + # Under 5 (including under 2) defaults to the CFM 2-4 instrument. + return "cfm_2_4" + + def _compute_age_restriction_enforced(self): + enforced = not self._disability_disregard_age() + for rec in self: + rec.age_restriction_enforced = enforced + @api.depends("age_at_assessment") def _compute_assessment_type(self): + disregard_age = self._disability_disregard_age() + for rec in self: + if disregard_age: + # Manual mode: preserve the user's selection; only seed a + # sensible default when nothing has been chosen yet. + if not rec.assessment_type: + rec.assessment_type = rec._assessment_type_for_age() + continue + rec.assessment_type = rec._assessment_type_for_age() + + @api.constrains("registrant_id") + def _check_birthdate_required_for_age(self): + """Require a date of birth when the assessment type is age-driven.""" + if self._disability_disregard_age(): + return for rec in self: - if rec.age_at_assessment >= 18: - rec.assessment_type = "wg_ss" - elif rec.age_at_assessment >= 5: - rec.assessment_type = "cfm_5_17" - elif rec.age_at_assessment >= 2: - rec.assessment_type = "cfm_2_4" - else: - # Under 2 - default to CFM 2-4 but flag for manual review - rec.assessment_type = "cfm_2_4" + if rec.registrant_id and not rec.registrant_id.birthdate: + raise ValidationError( + _( + "A date of birth is required for %s to determine the assessment type by age. " + "Set the registrant's date of birth, or enable 'Allow manual assessment type' " + "in the Disability Registry settings.", + rec.registrant_id.display_name, + ) + ) @api.depends("review_category", "assessment_date") def _compute_next_review_date(self): diff --git a/spp_disability_registry/models/res_config_settings.py b/spp_disability_registry/models/res_config_settings.py new file mode 100644 index 000000000..4ae914f30 --- /dev/null +++ b/spp_disability_registry/models/res_config_settings.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + # === Assessment Type === + disability_disregard_age = fields.Boolean( + string="Allow manual assessment type", + config_parameter="spp_disability_registry.disregard_age", + help="When enabled, the assessment type can be selected manually and the " + "registrant's date of birth is not required. When disabled (default), the " + "assessment type is determined automatically from the registrant's age and " + "a date of birth is required.", + ) diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index 105874e6b..7501aa01c 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -102,13 +102,20 @@ + - + - - -
    - Washington Group Short Set (WG-SS) + + + +
    +
    + Washington Group Short Set (WG-SS) +

    + Rate difficulty levels for each domain. + A person has a disability if they have "A lot of difficulty" or "Cannot do at all" in at least one domain. +

    +
    + + + + + + + + + + + + + + + + + + +
    + +
    + Child Functioning Module (CFM)

    - Rate difficulty levels for each domain. - A person has a disability if they have "A lot of difficulty" or "Cannot do at all" in at least one domain. + The CFM questionnaire for children is provided by the + Child Functioning Module support (CFM 2-4 and CFM 5-17).

    - - - - - - - - - - - - - - - - - -
    diff --git a/spp_disability_registry/views/menus.xml b/spp_disability_registry/views/menus.xml index d0cc06456..4da793738 100644 --- a/spp_disability_registry/views/menus.xml +++ b/spp_disability_registry/views/menus.xml @@ -35,4 +35,13 @@ sequence="100" groups="spp_disability_registry.group_disability_manager" /> + + + diff --git a/spp_disability_registry/views/res_config_settings_views.xml b/spp_disability_registry/views/res_config_settings_views.xml new file mode 100644 index 000000000..5fce175ec --- /dev/null +++ b/spp_disability_registry/views/res_config_settings_views.xml @@ -0,0 +1,36 @@ + + + + res.config.settings.view.form.inherit.disability + res.config.settings + + + + + + + + + + + + + + + + + Settings + res.config.settings + form + current + {'module': 'spp_disability_registry'} + + From 6a2e747903c7f909f8c75a4549f27efd10fa341a Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 3 Jun 2026 11:05:35 +0800 Subject: [PATCH 03/16] feat(disability_registry): proxy response by assessment type (#1053) - Remove the standalone 'Response Information' tab and move proxy fields to the top of the WG/CFM Assessment tab - Drive the proxy response flag by assessment type: forced on for CFM 2-4 and (unless self-report is enabled) CFM 5-17; optional for WG-SS - Add config: allow self-report on CFM 5-17 (+ optional minimum age) and allow proxy report on WG-SS (default on) - Add a 'Reason for Proxy' dropdown shown for WG-SS proxy responses - Manage the 'allow proxy on WG-SS' parameter explicitly so unticking the default-on setting persists (config_parameter set_param(False) deletes the param and the field default would re-tick it) --- spp_disability_registry/models/assessment.py | 83 ++++++++++++++++--- .../models/res_config_settings.py | 44 ++++++++++ .../views/assessment_views.xml | 37 +++++---- .../views/res_config_settings_views.xml | 28 +++++++ 4 files changed, 165 insertions(+), 27 deletions(-) diff --git a/spp_disability_registry/models/assessment.py b/spp_disability_registry/models/assessment.py index f2edc810a..804686459 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -164,8 +164,17 @@ class SppDisabilityAssessment(models.Model): # === Proxy Response Tracking === is_proxy_response = fields.Boolean( string="Proxy Response", - default=False, - help="True if responses provided by proxy (always true for children)", + compute="_compute_is_proxy_response", + store=True, + readonly=False, + help="True if responses were provided by a proxy. Forced on for CFM 2-4 and " + "(unless self-report is enabled) CFM 5-17; optional for WG-SS.", + ) + proxy_locked = fields.Boolean( + string="Proxy Locked", + compute="_compute_proxy_locked", + help="Technical flag: True when the proxy response checkbox is forced and " + "must not be edited (driven by assessment type, age and configuration).", ) proxy_respondent_id = fields.Many2one( "res.partner", @@ -182,6 +191,19 @@ class SppDisabilityAssessment(models.Model): ], string="Proxy Relationship", ) + proxy_reason = fields.Selection( + [ + ( + "functional_limitation", + "Unable to respond due to functional limitation (hearing, communication, cognitive, etc.)", + ), + ("caregiver", "Individual not present - caregiver responding"), + ("household_head", "Individual not present - household head responding"), + ("other", "Other"), + ], + string="Reason for Proxy", + help="Why a proxy responded instead of the individual (WG-SS).", + ) # === Computed Disability Indicator === has_disability = fields.Boolean( @@ -235,11 +257,56 @@ def _compute_age_at_assessment(self): rec.age_at_assessment = delta.years @api.model - def _disability_disregard_age(self): - """Read the 'disregard age for assessment type' configuration flag.""" + def _disability_config(self): + """Read the Disability Registry configuration parameters with defaults.""" # nosemgrep: odoo-sudo-without-context — standard Odoo pattern for system parameter access icp = self.env["ir.config_parameter"].sudo() - return icp.get_param("spp_disability_registry.disregard_age", "False") == "True" + get = icp.get_param + return { + "disregard_age": get("spp_disability_registry.disregard_age", "False") == "True", + "allow_self_report_cfm": get("spp_disability_registry.allow_self_report_cfm_5_17", "False") == "True", + "self_report_min_age": int(get("spp_disability_registry.self_report_min_age", "0") or 0), + "allow_proxy_wg_ss": get("spp_disability_registry.allow_proxy_wg_ss", "True") == "True", + } + + @api.model + def _disability_disregard_age(self): + """Read the 'disregard age for assessment type' configuration flag.""" + return self._disability_config()["disregard_age"] + + def _proxy_is_locked(self): + """Whether the proxy response flag is forced (non-editable) for this record. + + - CFM 2-4: always locked (proxy mandatory). + - CFM 5-17: locked unless self-report is enabled and the child meets the + optional minimum self-report age. + - WG-SS: locked only when proxy reporting is disabled by configuration. + """ + self.ensure_one() + cfg = self._disability_config() + if self.assessment_type == "cfm_2_4": + return True + if self.assessment_type == "cfm_5_17": + min_age = cfg["self_report_min_age"] + old_enough = (not min_age) or (self.age_at_assessment >= min_age) + return not (cfg["allow_self_report_cfm"] and old_enough) + # WG-SS (adult) and any fallback. + return not cfg["allow_proxy_wg_ss"] + + @api.depends("assessment_type") + def _compute_is_proxy_response(self): + """Seed the proxy flag from the assessment type (children = proxy by default, + adults = self-report). This default also matches the forced value in every + locked case, so locked records always carry the correct flag. Editable for + unlocked types — user toggles persist until the assessment type changes. + """ + for rec in self: + rec.is_proxy_response = rec.assessment_type in ("cfm_2_4", "cfm_5_17") + + @api.depends("assessment_type", "age_at_assessment") + def _compute_proxy_locked(self): + for rec in self: + rec.proxy_locked = rec._proxy_is_locked() def _assessment_type_for_age(self): """Return the WG/CFM assessment type implied by the age at assessment.""" @@ -318,12 +385,6 @@ def _compute_disability_indicator(self): rec.wg_domain_count = domain_count rec.has_disability = domain_count > 0 - @api.onchange("assessment_type") - def _onchange_assessment_type(self): - """Set proxy response flag automatically for child assessments.""" - if self.assessment_type in ("cfm_2_4", "cfm_5_17"): - self.is_proxy_response = True - def action_view_registrant(self): """Open the registrant form.""" self.ensure_one() diff --git a/spp_disability_registry/models/res_config_settings.py b/spp_disability_registry/models/res_config_settings.py index 4ae914f30..9463c2a5e 100644 --- a/spp_disability_registry/models/res_config_settings.py +++ b/spp_disability_registry/models/res_config_settings.py @@ -13,3 +13,47 @@ class ResConfigSettings(models.TransientModel): "assessment type is determined automatically from the registrant's age and " "a date of birth is required.", ) + + # === Proxy Response === + disability_allow_self_report_cfm = fields.Boolean( + string="Allow self-report on CFM 5-17", + config_parameter="spp_disability_registry.allow_self_report_cfm_5_17", + help="When enabled, the proxy response flag can be unticked on CFM 5-17 " + "assessments (subject to the minimum self-report age below). Disabled by " + "default — CFM 5-17 assessments are proxy responses.", + ) + disability_self_report_min_age = fields.Integer( + string="Minimum age for self-report (CFM 5-17)", + config_parameter="spp_disability_registry.self_report_min_age", + help="Minimum age at assessment at which self-report is allowed on CFM 5-17 " + "(only applies when self-report is enabled). 0 means no minimum.", + ) + # NB: managed manually below (not via config_parameter). A config_parameter + # Boolean defaulting to True cannot persist a False value: set_param(key, False) + # DELETES the parameter, and get_values then falls back to the field default + # (True), so the box re-ticks itself on save. Storing an explicit "True"/"False" + # string avoids the delete. + disability_allow_proxy_wg_ss = fields.Boolean( + string="Allow proxy report on WG-SS", + default=True, + help="When enabled (default), the proxy response flag can be ticked on adult " + "WG-SS assessments. When disabled, WG-SS assessments are self-report only.", + ) + + def get_values(self): + res = super().get_values() + # nosemgrep: odoo-sudo-without-context — standard Odoo pattern for system parameter access + icp = self.env["ir.config_parameter"].sudo() + res["disability_allow_proxy_wg_ss"] = ( + icp.get_param("spp_disability_registry.allow_proxy_wg_ss", "True") == "True" + ) + return res + + def set_values(self): + super().set_values() + # nosemgrep: odoo-sudo-without-context — standard Odoo pattern for system parameter access + icp = self.env["ir.config_parameter"].sudo() + icp.set_param( + "spp_disability_registry.allow_proxy_wg_ss", + "True" if self.disability_allow_proxy_wg_ss else "False", + ) diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index 7501aa01c..00c4ec83e 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -154,6 +154,27 @@ + + + + + + + + + + + +
    @@ -206,22 +227,6 @@ /> - - - - - - - - - - - - - diff --git a/spp_disability_registry/views/res_config_settings_views.xml b/spp_disability_registry/views/res_config_settings_views.xml index 5fce175ec..1d82b8971 100644 --- a/spp_disability_registry/views/res_config_settings_views.xml +++ b/spp_disability_registry/views/res_config_settings_views.xml @@ -20,6 +20,34 @@ + + + + +
    +
    +
    + + + +
    From cb4f3014d1e087e4e3709f13b0e62fb944e4d3e5 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 3 Jun 2026 12:06:53 +0800 Subject: [PATCH 04/16] feat(disability_registry): add CFM 2-4 questionnaire (#1048) - Add the WG/UNICEF Child Functioning Module 2-4 instrument (16 questions, CF1-CF16) as fields on the assessment, with the difficulty and behaviour response scales and the glasses/hearing-aid/walking-equipment branching - Compute has_disability per assessment type: CFM 2-4 uses the concluding answer per domain (standard threshold) plus the 'a lot more' behaviour threshold; WG-SS logic unchanged - Show the CFM 2-4 form only for the cfm_2_4 type; placeholder remains for cfm_5_17 (#1049) - Restyle both WG-SS and CFM 2-4 as carded questionnaires with horizontal radio options for clearer data entry --- spp_disability_registry/models/assessment.py | 164 +++++++- .../views/assessment_views.xml | 357 ++++++++++++++++-- 2 files changed, 484 insertions(+), 37 deletions(-) diff --git a/spp_disability_registry/models/assessment.py b/spp_disability_registry/models/assessment.py index 804686459..2814333de 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -18,6 +18,28 @@ # Severe difficulty levels that indicate disability per WG standard WG_SEVERE_DIFFICULTY_LEVELS = ("a_lot", "cannot") +# WG/UNICEF Child Functioning Module (CFM) response scales. +# Standard difficulty scale (shared by CFM 2-4 and CFM 5-17), including the +# survey non-response codes; "a_lot"/"cannot" reuse WG_SEVERE_DIFFICULTY_LEVELS. +CFM_DIFFICULTY_LEVELS = [ + ("none", "No difficulty"), + ("some", "Some difficulty"), + ("a_lot", "A lot of difficulty"), + ("cannot", "Cannot do at all"), + ("refused", "Refused"), + ("dont_know", "Don't know"), +] +# Behaviour-frequency scale used by CFM 2-4 CF16 (controlling behaviour). +# Disability threshold is "a lot more". +CFM_BEHAVIOR_LEVELS = [ + ("not_at_all", "Not at all"), + ("same_or_less", "The same or less"), + ("more", "More"), + ("a_lot_more", "A lot more"), + ("refused", "Refused"), + ("dont_know", "Don't know"), +] + # Review category to months mapping REVIEW_CATEGORY_MONTHS = { "mie": 12, # Medical Improvement Expected: 6-18 months (using 12) @@ -117,6 +139,73 @@ class SppDisabilityAssessment(models.Model): help="Do you have difficulty communicating (understanding or being understood)?", ) + # === CFM 2-4 Responses (children aged 2-4, CF1-CF16) === + # Vision + cfm24_glasses = fields.Boolean(string="Does the child wear glasses?") # CF1 + cfm24_vision_aided = fields.Selection( # CF2 (asked when glasses are worn) + CFM_DIFFICULTY_LEVELS, + string="When wearing glasses, difficulty seeing?", + ) + cfm24_vision = fields.Selection( # CF3 (asked when no glasses) + CFM_DIFFICULTY_LEVELS, + string="Difficulty seeing?", + ) + # Hearing + cfm24_hearing_aid = fields.Boolean(string="Does the child use a hearing aid?") # CF4 + cfm24_hearing_aided = fields.Selection( # CF5 + CFM_DIFFICULTY_LEVELS, + string="When using a hearing aid, difficulty hearing?", + ) + cfm24_hearing = fields.Selection( # CF6 + CFM_DIFFICULTY_LEVELS, + string="Difficulty hearing sounds like voices or music?", + ) + # Mobility + cfm24_walk_equipment = fields.Boolean( # CF7 + string="Does the child use equipment or assistance for walking?" + ) + cfm24_walk_unaided = fields.Selection( # CF8 (without equipment) + CFM_DIFFICULTY_LEVELS, + string="Without equipment/assistance, difficulty walking?", + ) + cfm24_walk_aided = fields.Selection( # CF9 (with equipment) - concluding when equipment used + CFM_DIFFICULTY_LEVELS, + string="With equipment/assistance, difficulty walking?", + ) + cfm24_walk_compare = fields.Selection( # CF10 (no equipment) - concluding when no equipment + CFM_DIFFICULTY_LEVELS, + string="Compared with other children, difficulty walking?", + ) + # Dexterity + cfm24_dexterity = fields.Selection( # CF11 + CFM_DIFFICULTY_LEVELS, + string="Difficulty picking up small objects with the hand?", + ) + # Communication + cfm24_understand_you = fields.Selection( # CF12 + CFM_DIFFICULTY_LEVELS, + string="Difficulty understanding you?", + ) + cfm24_understood = fields.Selection( # CF13 + CFM_DIFFICULTY_LEVELS, + string="When the child speaks, difficulty understanding him/her?", + ) + # Learning + cfm24_learning = fields.Selection( # CF14 + CFM_DIFFICULTY_LEVELS, + string="Difficulty learning things?", + ) + # Playing + cfm24_playing = fields.Selection( # CF15 + CFM_DIFFICULTY_LEVELS, + string="Difficulty playing?", + ) + # Controlling behaviour + cfm24_behavior = fields.Selection( # CF16 (behaviour scale, threshold "a lot more") + CFM_BEHAVIOR_LEVELS, + string="How much does the child kick, bite or hit others?", + ) + # === Impairment Classification (DCI DO.DR.02) === impairment_type_ids = fields.Many2many( "spp.vocabulary.code", @@ -361,27 +450,80 @@ def _compute_next_review_date(self): months = REVIEW_CATEGORY_MONTHS.get(rec.review_category, REVIEW_CATEGORY_MONTHS["mip"]) rec.next_review_date = rec.assessment_date + relativedelta(months=months) + def _wg_ss_domain_count(self): + """Number of WG-SS domains with 'a lot of difficulty' or 'cannot do at all'.""" + self.ensure_one() + responses = [ + self.wg_seeing, + self.wg_hearing, + self.wg_walking, + self.wg_remembering, + self.wg_selfcare, + self.wg_communicating, + ] + return sum(1 for r in responses if r in WG_SEVERE_DIFFICULTY_LEVELS) + + def _cfm_2_4_domain_count(self): + """Number of CFM 2-4 domains meeting the disability threshold. + + Standard domains use 'a lot of difficulty'/'cannot do at all' on the + concluding answer; controlling behaviour (CF16) uses 'a lot more'. + """ + self.ensure_one() + # Concluding answer per branched domain. + vision = self.cfm24_vision_aided if self.cfm24_glasses else self.cfm24_vision + hearing = self.cfm24_hearing_aided if self.cfm24_hearing_aid else self.cfm24_hearing + mobility = self.cfm24_walk_aided if self.cfm24_walk_equipment else self.cfm24_walk_compare + standard = [ + vision, + hearing, + mobility, + self.cfm24_dexterity, + self.cfm24_understand_you, + self.cfm24_understood, + self.cfm24_learning, + self.cfm24_playing, + ] + count = sum(1 for r in standard if r in WG_SEVERE_DIFFICULTY_LEVELS) + if self.cfm24_behavior == "a_lot_more": + count += 1 + return count + @api.depends( + "assessment_type", "wg_seeing", "wg_hearing", "wg_walking", "wg_remembering", "wg_selfcare", "wg_communicating", + "cfm24_glasses", + "cfm24_vision_aided", + "cfm24_vision", + "cfm24_hearing_aid", + "cfm24_hearing_aided", + "cfm24_hearing", + "cfm24_walk_equipment", + "cfm24_walk_aided", + "cfm24_walk_compare", + "cfm24_dexterity", + "cfm24_understand_you", + "cfm24_understood", + "cfm24_learning", + "cfm24_playing", + "cfm24_behavior", ) def _compute_disability_indicator(self): - """WG standard: any domain with 'a_lot' or 'cannot' indicates disability.""" + """Count domains meeting the disability threshold for the active instrument. + + Any domain at/above threshold marks the person as having a disability. + """ for rec in self: - responses = [ - rec.wg_seeing, - rec.wg_hearing, - rec.wg_walking, - rec.wg_remembering, - rec.wg_selfcare, - rec.wg_communicating, - ] - # Count domains with severe difficulty - domain_count = sum(1 for r in responses if r in WG_SEVERE_DIFFICULTY_LEVELS) + if rec.assessment_type == "cfm_2_4": + domain_count = rec._cfm_2_4_domain_count() + else: + # WG-SS, and CFM 5-17 until its own instrument lands (#1049). + domain_count = rec._wg_ss_domain_count() rec.wg_domain_count = domain_count rec.has_disability = domain_count > 0 diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index 00c4ec83e..9be7e08de 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -87,7 +87,7 @@
    Disability Identified

    - Based on WG responses, this person meets the disability threshold + Based on the assessment responses, this person meets the disability threshold ( Washington Group Short Set (WG-SS)

    - Rate difficulty levels for each domain. - A person has a disability if they have "A lot of difficulty" or "Cannot do at all" in at least one domain. + Rate the difficulty for each domain. A person has a disability + if any domain is "A lot of difficulty" or "Cannot do at all".

    - - - - - - - - - - - - - - - - - - +
    +
    + Functional Domains +
    +
    +
    +
    Difficulty seeing, even if wearing glasses?
    + +
    +
    +
    Difficulty hearing, even if using a hearing aid?
    + +
    +
    +
    Difficulty walking or climbing steps?
    + +
    +
    +
    Difficulty remembering or concentrating?
    + +
    +
    +
    Difficulty with self-care (washing, dressing)?
    + +
    +
    +
    Difficulty communicating (understanding or being understood)?
    + +
    +
    +
    +
    + +
    +
    + Child Functioning Module - Ages 2-4 (CFM 2-4) +

    + A child meets the disability threshold if any domain is + answered "A lot of difficulty"/"Cannot do at all" (or, for + controlling behaviour, "A lot more"). +

    +
    + +
    +
    + Vision +
    +
    +
    + + Does the child wear glasses? +
    +
    +
    When wearing glasses, difficulty seeing?
    + +
    +
    +
    Difficulty seeing?
    + +
    +
    +
    + +
    +
    + Hearing +
    +
    +
    + + Does the child use a hearing aid? +
    +
    +
    When using a hearing aid, difficulty hearing?
    + +
    +
    +
    Difficulty hearing sounds like voices or music?
    + +
    +
    +
    + +
    +
    + Mobility +
    +
    +
    + + Does the child use equipment or assistance for walking? +
    +
    +
    Without equipment/assistance, difficulty walking?
    + +
    +
    +
    With equipment/assistance, difficulty walking?
    + +
    +
    +
    Compared with other children, difficulty walking?
    + +
    +
    +
    + +
    +
    + Dexterity, Communication, Learning and Playing +
    +
    +
    +
    Difficulty picking up small objects with the hand?
    + +
    +
    +
    Difficulty understanding you?
    + +
    +
    +
    When the child speaks, difficulty understanding him/her?
    + +
    +
    +
    Difficulty learning things?
    + +
    +
    +
    Difficulty playing?
    + +
    +
    +
    + +
    +
    + Controlling Behaviour +
    +
    +
    +
    Compared with other children, how much does the child kick, bite or hit others?
    + +
    +
    +
    - +
    - Child Functioning Module (CFM) + Child Functioning Module - Ages 5-17 (CFM 5-17)

    - The CFM questionnaire for children is provided by the - Child Functioning Module support (CFM 2-4 and CFM 5-17). + The CFM 5-17 questionnaire is provided by the Child Functioning + Module 5-17 support.

    From 129411d29e6897ea45f421b2ace4d6facf4b24a8 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 3 Jun 2026 13:02:50 +0800 Subject: [PATCH 05/16] feat(disability_registry): add CFM 5-17 questionnaire (#1049) - Add the WG/UNICEF Child Functioning Module 5-17 instrument (24 questions, CF1-CF24) as fields on the assessment, with the difficulty scale, the frequency scale for anxiety/depression, and the glasses/hearing-aid/ walking-equipment branching (100 m / 500 m distances) - Score has_disability for CFM 5-17: standard threshold on concluding answers; mobility uses the with-aid answer (either distance) when equipment is used, otherwise the compared-with-peers answer; anxiety and depression use a 'daily' frequency threshold - Show the CFM 5-17 carded questionnaire for the cfm_5_17 type, replacing the placeholder; same horizontal-radio layout as CFM 2-4 --- spp_disability_registry/models/assessment.py | 178 ++++++++- .../views/assessment_views.xml | 371 +++++++++++++++++- 2 files changed, 536 insertions(+), 13 deletions(-) diff --git a/spp_disability_registry/models/assessment.py b/spp_disability_registry/models/assessment.py index 2814333de..b45e4425f 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -39,6 +39,17 @@ ("refused", "Refused"), ("dont_know", "Don't know"), ] +# Frequency scale used by CFM 5-17 CF23 (anxiety) and CF24 (depression). +# Disability threshold is "daily". +CFM_FREQUENCY_LEVELS = [ + ("daily", "Daily"), + ("weekly", "Weekly"), + ("monthly", "Monthly"), + ("few_times_year", "A few times a year"), + ("never", "Never"), + ("refused", "Refused"), + ("dont_know", "Don't know"), +] # Review category to months mapping REVIEW_CATEGORY_MONTHS = { @@ -206,6 +217,110 @@ class SppDisabilityAssessment(models.Model): string="How much does the child kick, bite or hit others?", ) + # === CFM 5-17 Responses (children aged 5-17, CF1-CF24) === + # Vision + cfm517_glasses = fields.Boolean(string="Does the child wear glasses?") # CF1 + cfm517_vision_aided = fields.Selection( # CF2 + CFM_DIFFICULTY_LEVELS, + string="When wearing glasses, difficulty seeing?", + ) + cfm517_vision = fields.Selection( # CF3 + CFM_DIFFICULTY_LEVELS, + string="Difficulty seeing?", + ) + # Hearing + cfm517_hearing_aid = fields.Boolean(string="Does the child use a hearing aid?") # CF4 + cfm517_hearing_aided = fields.Selection( # CF5 + CFM_DIFFICULTY_LEVELS, + string="When using a hearing aid, difficulty hearing?", + ) + cfm517_hearing = fields.Selection( # CF6 + CFM_DIFFICULTY_LEVELS, + string="Difficulty hearing sounds like voices or music?", + ) + # Mobility (100m / 500m, with and without equipment; concluding = the "with aid" + # answer when equipment is used, otherwise the "compared with peers" answer) + cfm517_walk_equipment = fields.Boolean( # CF7 + string="Does the child use equipment or assistance for walking?" + ) + cfm517_walk_unaided_100 = fields.Selection( # CF8 (without equipment, 100m) + CFM_DIFFICULTY_LEVELS, + string="Without equipment, difficulty walking 100 m on level ground?", + ) + cfm517_walk_unaided_500 = fields.Selection( # CF9 (without equipment, 500m) + CFM_DIFFICULTY_LEVELS, + string="Without equipment, difficulty walking 500 m on level ground?", + ) + cfm517_walk_aided_100 = fields.Selection( # CF10 (with equipment, 100m) - concluding + CFM_DIFFICULTY_LEVELS, + string="With equipment, difficulty walking 100 m on level ground?", + ) + cfm517_walk_aided_500 = fields.Selection( # CF11 (with equipment, 500m) - concluding + CFM_DIFFICULTY_LEVELS, + string="With equipment, difficulty walking 500 m on level ground?", + ) + cfm517_walk_compare_100 = fields.Selection( # CF12 (no equipment, compared, 100m) - concluding + CFM_DIFFICULTY_LEVELS, + string="Compared with peers, difficulty walking 100 m on level ground?", + ) + cfm517_walk_compare_500 = fields.Selection( # CF13 (no equipment, compared, 500m) - concluding + CFM_DIFFICULTY_LEVELS, + string="Compared with peers, difficulty walking 500 m on level ground?", + ) + # Self-care + cfm517_selfcare = fields.Selection( # CF14 + CFM_DIFFICULTY_LEVELS, + string="Difficulty with self-care such as feeding or dressing?", + ) + # Communication + cfm517_comm_inside = fields.Selection( # CF15 + CFM_DIFFICULTY_LEVELS, + string="Difficulty being understood by people inside the household?", + ) + cfm517_comm_outside = fields.Selection( # CF16 + CFM_DIFFICULTY_LEVELS, + string="Difficulty being understood by people outside the household?", + ) + # Learning + cfm517_learning = fields.Selection( # CF17 + CFM_DIFFICULTY_LEVELS, + string="Difficulty learning things?", + ) + # Remembering + cfm517_remembering = fields.Selection( # CF18 + CFM_DIFFICULTY_LEVELS, + string="Difficulty remembering things?", + ) + # Concentrating + cfm517_concentrating = fields.Selection( # CF19 + CFM_DIFFICULTY_LEVELS, + string="Difficulty concentrating on an activity he/she enjoys?", + ) + # Accepting change + cfm517_accepting_change = fields.Selection( # CF20 + CFM_DIFFICULTY_LEVELS, + string="Difficulty accepting changes in routine?", + ) + # Controlling behaviour (standard difficulty scale in CFM 5-17) + cfm517_behavior = fields.Selection( # CF21 + CFM_DIFFICULTY_LEVELS, + string="Difficulty controlling his/her behaviour?", + ) + # Making friends + cfm517_friends = fields.Selection( # CF22 + CFM_DIFFICULTY_LEVELS, + string="Difficulty making friends?", + ) + # Anxiety / Depression (frequency scale, threshold "daily") + cfm517_anxiety = fields.Selection( # CF23 + CFM_FREQUENCY_LEVELS, + string="How often does the child seem very anxious, nervous or worried?", + ) + cfm517_depression = fields.Selection( # CF24 + CFM_FREQUENCY_LEVELS, + string="How often does the child seem very sad or depressed?", + ) + # === Impairment Classification (DCI DO.DR.02) === impairment_type_ids = fields.Many2many( "spp.vocabulary.code", @@ -489,6 +604,44 @@ def _cfm_2_4_domain_count(self): count += 1 return count + def _cfm_5_17_domain_count(self): + """Number of CFM 5-17 domains meeting the disability threshold. + + Standard domains use 'a lot of difficulty'/'cannot do at all' on the + concluding answer; mobility uses the 'with aid' answer (either distance) + when equipment is used, otherwise the 'compared with peers' answer; + anxiety (CF23) and depression (CF24) use a 'daily' frequency threshold. + """ + self.ensure_one() + severe = WG_SEVERE_DIFFICULTY_LEVELS + vision = self.cfm517_vision_aided if self.cfm517_glasses else self.cfm517_vision + hearing = self.cfm517_hearing_aided if self.cfm517_hearing_aid else self.cfm517_hearing + if self.cfm517_walk_equipment: + mobility_severe = self.cfm517_walk_aided_100 in severe or self.cfm517_walk_aided_500 in severe + else: + mobility_severe = self.cfm517_walk_compare_100 in severe or self.cfm517_walk_compare_500 in severe + standard = [ + vision, + hearing, + self.cfm517_selfcare, + self.cfm517_comm_inside, + self.cfm517_comm_outside, + self.cfm517_learning, + self.cfm517_remembering, + self.cfm517_concentrating, + self.cfm517_accepting_change, + self.cfm517_behavior, + self.cfm517_friends, + ] + count = sum(1 for r in standard if r in severe) + if mobility_severe: + count += 1 + if self.cfm517_anxiety == "daily": + count += 1 + if self.cfm517_depression == "daily": + count += 1 + return count + @api.depends( "assessment_type", "wg_seeing", @@ -512,6 +665,28 @@ def _cfm_2_4_domain_count(self): "cfm24_learning", "cfm24_playing", "cfm24_behavior", + "cfm517_glasses", + "cfm517_vision_aided", + "cfm517_vision", + "cfm517_hearing_aid", + "cfm517_hearing_aided", + "cfm517_hearing", + "cfm517_walk_equipment", + "cfm517_walk_aided_100", + "cfm517_walk_aided_500", + "cfm517_walk_compare_100", + "cfm517_walk_compare_500", + "cfm517_selfcare", + "cfm517_comm_inside", + "cfm517_comm_outside", + "cfm517_learning", + "cfm517_remembering", + "cfm517_concentrating", + "cfm517_accepting_change", + "cfm517_behavior", + "cfm517_friends", + "cfm517_anxiety", + "cfm517_depression", ) def _compute_disability_indicator(self): """Count domains meeting the disability threshold for the active instrument. @@ -521,8 +696,9 @@ def _compute_disability_indicator(self): for rec in self: if rec.assessment_type == "cfm_2_4": domain_count = rec._cfm_2_4_domain_count() + elif rec.assessment_type == "cfm_5_17": + domain_count = rec._cfm_5_17_domain_count() else: - # WG-SS, and CFM 5-17 until its own instrument lands (#1049). domain_count = rec._wg_ss_domain_count() rec.wg_domain_count = domain_count rec.has_disability = domain_count > 0 diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index 9be7e08de..044c95595 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -507,18 +507,365 @@
    - -
    - Child Functioning Module - Ages 5-17 (CFM 5-17) -

    - The CFM 5-17 questionnaire is provided by the Child Functioning - Module 5-17 support. -

    + +
    +
    + Child Functioning Module - Ages 5-17 (CFM 5-17) +

    + A child meets the disability threshold if any domain is + answered "A lot of difficulty"/"Cannot do at all" (or, for + anxiety and depression, occurs "Daily"). +

    +
    + +
    +
    + Vision +
    +
    +
    + + Does the child wear glasses? +
    +
    +
    When wearing glasses, difficulty seeing?
    + +
    +
    +
    Difficulty seeing?
    + +
    +
    +
    + +
    +
    + Hearing +
    +
    +
    + + Does the child use a hearing aid? +
    +
    +
    When using a hearing aid, difficulty hearing?
    + +
    +
    +
    Difficulty hearing sounds like voices or music?
    + +
    +
    +
    + +
    +
    + Mobility +
    +
    +
    + + Does the child use equipment or assistance for walking? +
    +
    +
    Without equipment, difficulty walking 100 m on level ground?
    + +
    +
    +
    Without equipment, difficulty walking 500 m on level ground?
    + +
    +
    +
    With equipment, difficulty walking 100 m on level ground?
    + +
    +
    +
    With equipment, difficulty walking 500 m on level ground?
    + +
    +
    +
    Compared with peers, difficulty walking 100 m on level ground?
    + +
    +
    +
    Compared with peers, difficulty walking 500 m on level ground?
    + +
    +
    +
    + +
    +
    + Self-care and Communication +
    +
    +
    +
    Difficulty with self-care such as feeding or dressing?
    + +
    +
    +
    Difficulty being understood by people inside the household?
    + +
    +
    +
    Difficulty being understood by people outside the household?
    + +
    +
    +
    + +
    +
    + Cognition and Behaviour +
    +
    +
    +
    Difficulty learning things?
    + +
    +
    +
    Difficulty remembering things?
    + +
    +
    +
    Difficulty concentrating on an activity he/she enjoys?
    + +
    +
    +
    Difficulty accepting changes in routine?
    + +
    +
    +
    Difficulty controlling his/her behaviour?
    + +
    +
    +
    Difficulty making friends?
    + +
    +
    +
    + +
    +
    + Emotional Wellbeing +
    +
    +
    +
    How often does the child seem very anxious, nervous or worried?
    + +
    +
    +
    How often does the child seem very sad or depressed?
    + +
    +
    +
    From f1eec6236e6b3f77d9476c548b826851e97b6a2b Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 4 Jun 2026 11:26:41 +0800 Subject: [PATCH 06/16] feat(disability_registry): impairment classification on its own tab, multi-row (#1054) - Add spp.disability.impairment line model (type + cause + severity per row) with ACLs; expose as impairment_line_ids on the assessment - Move Impairment Classification to its own tab with an editable list; Overview shows a read-only Classification Summary - Derive the assessment-level severity_level_id (most severe line) and impairment_type_ids (union) from the lines so the registrant propagation, list/badge decorations, filters, concept groups and CEL functions keep working unchanged; drop the per-assessment impairment_cause_id - Update demo data to use impairment lines (adult now has two) - Update tests to record severity via impairment lines; fix a stale test that called the _onchange_assessment_type removed in #1053 --- spp_disability_registry/demo/demo.xml | 27 ++++++++++--- spp_disability_registry/models/__init__.py | 1 + spp_disability_registry/models/assessment.py | 37 ++++++++++++++---- spp_disability_registry/models/impairment.py | 38 ++++++++++++++++++ .../security/ir.model.access.csv | 4 ++ .../tests/test_assessment.py | 6 +-- .../tests/test_cel_functions.py | 17 ++++++-- .../tests/test_registrant.py | 38 +++++++++++++++--- .../views/assessment_views.xml | 39 ++++++++++++++----- 9 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 spp_disability_registry/models/impairment.py diff --git a/spp_disability_registry/demo/demo.xml b/spp_disability_registry/demo/demo.xml index 0ba91aa49..25f018a8b 100644 --- a/spp_disability_registry/demo/demo.xml +++ b/spp_disability_registry/demo/demo.xml @@ -39,10 +39,20 @@ a_lot some - mine none a_lot - mip True parent diff --git a/spp_disability_registry/models/__init__.py b/spp_disability_registry/models/__init__.py index 6578c1071..c92b55a5a 100644 --- a/spp_disability_registry/models/__init__.py +++ b/spp_disability_registry/models/__init__.py @@ -1,5 +1,6 @@ from . import assessment from . import assistive_device +from . import impairment from . import cel_disability_functions from . import registrant from . import res_config_settings diff --git a/spp_disability_registry/models/assessment.py b/spp_disability_registry/models/assessment.py index b45e4425f..89adfeb7f 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -321,27 +321,48 @@ class SppDisabilityAssessment(models.Model): string="How often does the child seem very sad or depressed?", ) - # === Impairment Classification (DCI DO.DR.02) === + # === Impairment Classification (DCI vocabularies) === + # One row per impairment type, each with its own cause and severity. + impairment_line_ids = fields.One2many( + "spp.disability.impairment", + "assessment_id", + string="Impairment Classification", + ) + # Assessment-level summaries derived from the lines, kept for the registrant + # propagation, list/badge decorations, search filters and CEL functions. impairment_type_ids = fields.Many2many( "spp.vocabulary.code", "spp_disability_assessment_impairment_type_rel", "assessment_id", "code_id", string="Impairment Types", - domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:01')]", - ) - impairment_cause_id = fields.Many2one( - "spp.vocabulary.code", - string="Impairment Cause", - domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:03')]", + compute="_compute_impairment_summary", + store=True, ) severity_level_id = fields.Many2one( "spp.vocabulary.code", string="Severity Level", - domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:02')]", + compute="_compute_impairment_summary", + store=True, tracking=True, + help="Overall severity: the most severe level across the impairment classification lines.", ) + @api.depends( + "impairment_line_ids.impairment_type_id", + "impairment_line_ids.severity_level_id", + "impairment_line_ids.severity_sequence", + ) + def _compute_impairment_summary(self): + for rec in self: + rec.impairment_type_ids = rec.impairment_line_ids.impairment_type_id + severe_lines = rec.impairment_line_ids.filtered("severity_level_id") + if severe_lines: + top = max(severe_lines, key=lambda line: line.severity_sequence or 0) + rec.severity_level_id = top.severity_level_id + else: + rec.severity_level_id = False + # === Review Schedule (categorical) === review_category = fields.Selection( [ diff --git a/spp_disability_registry/models/impairment.py b/spp_disability_registry/models/impairment.py new file mode 100644 index 000000000..f60a5d633 --- /dev/null +++ b/spp_disability_registry/models/impairment.py @@ -0,0 +1,38 @@ +from odoo import fields, models + + +class SppDisabilityImpairment(models.Model): + _name = "spp.disability.impairment" + _description = "Disability Impairment Classification" + _rec_name = "impairment_type_id" + _order = "severity_sequence desc, id" + + assessment_id = fields.Many2one( + "spp.disability.assessment", + string="Assessment", + required=True, + ondelete="cascade", + index=True, + ) + impairment_type_id = fields.Many2one( + "spp.vocabulary.code", + string="Impairment Type", + required=True, + domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:01')]", + ) + impairment_cause_id = fields.Many2one( + "spp.vocabulary.code", + string="Impairment Cause", + domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:03')]", + ) + severity_level_id = fields.Many2one( + "spp.vocabulary.code", + string="Severity Level", + domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:02')]", + ) + # Stored so the assessment can roll up the "most severe" line and the list + # can order by severity. + severity_sequence = fields.Integer( + related="severity_level_id.sequence", + store=True, + ) diff --git a/spp_disability_registry/security/ir.model.access.csv b/spp_disability_registry/security/ir.model.access.csv index c2ca0a345..1d843b49a 100644 --- a/spp_disability_registry/security/ir.model.access.csv +++ b/spp_disability_registry/security/ir.model.access.csv @@ -7,3 +7,7 @@ access_spp_assistive_device_viewer,spp.assistive.device viewer,model_spp_assisti access_spp_assistive_device_assessor,spp.assistive.device assessor,model_spp_assistive_device,group_disability_assessor,1,1,1,0 access_spp_assistive_device_validator,spp.assistive.device validator,model_spp_assistive_device,group_disability_validator,1,1,1,0 access_spp_assistive_device_manager,spp.assistive.device manager,model_spp_assistive_device,group_disability_manager,1,1,1,1 +access_spp_disability_impairment_viewer,spp.disability.impairment viewer,model_spp_disability_impairment,group_disability_viewer,1,0,0,0 +access_spp_disability_impairment_assessor,spp.disability.impairment assessor,model_spp_disability_impairment,group_disability_assessor,1,1,1,1 +access_spp_disability_impairment_validator,spp.disability.impairment validator,model_spp_disability_impairment,group_disability_validator,1,1,1,1 +access_spp_disability_impairment_manager,spp.disability.impairment manager,model_spp_disability_impairment,group_disability_manager,1,1,1,1 diff --git a/spp_disability_registry/tests/test_assessment.py b/spp_disability_registry/tests/test_assessment.py index e1b8fa290..d468ca8c4 100644 --- a/spp_disability_registry/tests/test_assessment.py +++ b/spp_disability_registry/tests/test_assessment.py @@ -281,15 +281,15 @@ def test_assessment_name_computed(self): # === Proxy Response Tests === def test_proxy_flag_set_for_child(self): - """Test that proxy response flag is set for child assessments.""" + """Proxy response flag is set automatically for child (CFM) assessments.""" assessment = self.env["spp.disability.assessment"].create( { "registrant_id": self.child_registrant.id, "assessment_date": date.today(), } ) - # Trigger onchange - assessment._onchange_assessment_type() + # is_proxy_response is computed from the (age-derived) assessment type. + self.assertIn(assessment.assessment_type, ("cfm_2_4", "cfm_5_17")) self.assertTrue(assessment.is_proxy_response) # === Date Validation Tests === diff --git a/spp_disability_registry/tests/test_cel_functions.py b/spp_disability_registry/tests/test_cel_functions.py index c0541ae1a..2cfc06fc3 100644 --- a/spp_disability_registry/tests/test_cel_functions.py +++ b/spp_disability_registry/tests/test_cel_functions.py @@ -95,6 +95,11 @@ def setUpClass(cls): ], limit=1, ) + # Severity is recorded on impairment lines; grab an impairment type. + cls.impairment_type = cls.env["spp.vocabulary.code"].search( + [("vocabulary_id.namespace_uri", "=", "urn:dci:cd:dr:01")], + limit=1, + ) # Create approved assessment for member_with_disability (mild) assessment1 = cls.env["spp.disability.assessment"].create( @@ -102,7 +107,9 @@ def setUpClass(cls): "registrant_id": cls.member_with_disability.id, "assessment_date": date.today(), "wg_walking": "a_lot", - "severity_level_id": cls.severity_mild.id if cls.severity_mild else False, + "impairment_line_ids": [ + (0, 0, {"impairment_type_id": cls.impairment_type.id, "severity_level_id": cls.severity_mild.id}) + ], } ) assessment1.write({"approval_state": "approved"}) @@ -114,7 +121,9 @@ def setUpClass(cls): "assessment_date": date.today(), "wg_seeing": "cannot", "wg_hearing": "cannot", - "severity_level_id": cls.severity_severe.id if cls.severity_severe else False, + "impairment_line_ids": [ + (0, 0, {"impairment_type_id": cls.impairment_type.id, "severity_level_id": cls.severity_severe.id}) + ], "review_category": "mine", } ) @@ -300,7 +309,9 @@ def test_needs_reassessment_true_when_due(self): "registrant_id": overdue_registrant.id, "assessment_date": date.today() - relativedelta(years=2), "wg_walking": "a_lot", - "severity_level_id": self.severity_mild.id if self.severity_mild else False, + "impairment_line_ids": [ + (0, 0, {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_mild.id}) + ], "review_category": "mie", # 12 months, so overdue } ) diff --git a/spp_disability_registry/tests/test_registrant.py b/spp_disability_registry/tests/test_registrant.py index 03d5d93b9..dbe1b3c0d 100644 --- a/spp_disability_registry/tests/test_registrant.py +++ b/spp_disability_registry/tests/test_registrant.py @@ -112,6 +112,12 @@ def setUpClass(cls): limit=1, ) + # Get an impairment type (severity is recorded on impairment lines) + cls.impairment_type = cls.env["spp.vocabulary.code"].search( + [("vocabulary_id.namespace_uri", "=", "urn:dci:cd:dr:01")], + limit=1, + ) + # Get device type cls.device_wheelchair = cls.env["spp.vocabulary.code"].search( [ @@ -135,7 +141,9 @@ def test_no_disability_with_draft_assessment(self): "registrant_id": self.registrant.id, "assessment_date": date.today(), "wg_seeing": "a_lot", - "severity_level_id": self.severity_mild.id if self.severity_mild else False, + "impairment_line_ids": [ + (0, 0, {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_mild.id}) + ], } ) # Recompute @@ -149,7 +157,13 @@ def test_disability_with_approved_assessment(self): "registrant_id": self.registrant.id, "assessment_date": date.today(), "wg_seeing": "a_lot", - "severity_level_id": self.severity_severe.id if self.severity_severe else False, + "impairment_line_ids": [ + ( + 0, + 0, + {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_severe.id}, + ) + ], "review_category": "mip", } ) @@ -172,7 +186,9 @@ def test_latest_approved_assessment_used(self): "registrant_id": self.registrant.id, "assessment_date": date.today() - relativedelta(months=6), "wg_seeing": "a_lot", - "severity_level_id": self.severity_mild.id if self.severity_mild else False, + "impairment_line_ids": [ + (0, 0, {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_mild.id}) + ], } ) old_assessment.write({"approval_state": "approved"}) @@ -183,7 +199,13 @@ def test_latest_approved_assessment_used(self): "registrant_id": self.registrant.id, "assessment_date": date.today(), "wg_seeing": "cannot", - "severity_level_id": self.severity_severe.id if self.severity_severe else False, + "impairment_line_ids": [ + ( + 0, + 0, + {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_severe.id}, + ) + ], } ) new_assessment.write({"approval_state": "approved"}) @@ -306,7 +328,13 @@ def test_household_member_disability_independent(self): "registrant_id": self.member1.id, "assessment_date": date.today(), "wg_walking": "cannot", - "severity_level_id": self.severity_severe.id if self.severity_severe else False, + "impairment_line_ids": [ + ( + 0, + 0, + {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_severe.id}, + ) + ], } ) assessment.write({"approval_state": "approved"}) diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index 044c95595..df8337b33 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -130,11 +130,36 @@ - + + + + + + + + + + + + +
    + Impairment Classification +

    + Add one row per impairment type, each with its own cause + and severity. The overall severity (shown on the Overview + and the registrant) is the most severe level across the rows. +

    +
    + + + -
    - - - - - + +
    - + From a2e9b8f83c98971b8c82c299586f084b5addd15c Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 4 Jun 2026 12:39:25 +0800 Subject: [PATCH 07/16] fix(disability_registry): configurable assessment approval workflow (#1060) - Make the assessment use a configured approval definition: override _get_approval_definition / _resolve_approval_definition to return the workflow selected in Disability Registry settings (same pattern as program cycles/entitlements and change-request types) - Add 'Assessment approval workflow' to Disability Registry settings, a reference to an spp.approval.definition for the assessment model (created in Approvals > Approval Definitions); no default is seeded - Hide Submit and show a guidance alert until a workflow is configured - Gate Approve/Reject by the computed can_approve/can_reject so the configured approvers drive the buttons instead of a fixed group --- spp_disability_registry/models/assessment.py | 26 +++++++++++++ .../models/res_config_settings.py | 15 +++++++ .../views/assessment_views.xml | 39 ++++++++++++++++--- .../views/res_config_settings_views.xml | 12 ++++++ 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/spp_disability_registry/models/assessment.py b/spp_disability_registry/models/assessment.py index 89adfeb7f..5838b084b 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -117,6 +117,12 @@ class SppDisabilityAssessment(models.Model): "auto-determined by age (default). Toggled by the 'Allow manual assessment " "type' setting in Disability Registry configuration.", ) + has_approval_definition = fields.Boolean( + string="Approval Workflow Configured", + compute="_compute_has_approval_definition", + help="Technical flag: whether an approval workflow is configured for " + "disability assessments. The Submit button is hidden until one exists.", + ) # === WG-SS Responses (6 domains) === wg_seeing = fields.Selection( @@ -549,6 +555,26 @@ def _compute_age_restriction_enforced(self): for rec in self: rec.age_restriction_enforced = enforced + def _compute_has_approval_definition(self): + for rec in self: + rec.has_approval_definition = bool(rec._resolve_approval_definition()) + + def _get_approval_definition(self): + """Return the approval workflow configured for disability assessments + (Settings > Disability Registry), or an empty recordset if none is set. + """ + definition = self.env["spp.approval.definition"] + # nosemgrep: odoo-sudo-without-context — read approval definition id from config + def_id = self.env["ir.config_parameter"].sudo().get_param("spp_disability_registry.approval_definition_id") + return definition.browse(int(def_id)).exists() if def_id else definition + + def _resolve_approval_definition(self): + """Use only the configured definition (no model-wide fallback), so the + Submit button stays hidden until an admin selects a workflow. + """ + self.ensure_one() + return self._get_approval_definition() + @api.depends("age_at_assessment") def _compute_assessment_type(self): disregard_age = self._disability_disregard_age() diff --git a/spp_disability_registry/models/res_config_settings.py b/spp_disability_registry/models/res_config_settings.py index 9463c2a5e..da4d62c57 100644 --- a/spp_disability_registry/models/res_config_settings.py +++ b/spp_disability_registry/models/res_config_settings.py @@ -40,6 +40,21 @@ class ResConfigSettings(models.TransientModel): "WG-SS assessments. When disabled, WG-SS assessments are self-report only.", ) + # === Approval === + # The approval workflow applied to disability assessments. Create the workflow in + # Approvals > Approval Definitions (Model = Disability Assessment), then select it + # here. The assessment reads it via _get_approval_definition(). + disability_approval_definition_id = fields.Many2one( + "spp.approval.definition", + string="Assessment approval workflow", + domain="[('model', '=', 'spp.disability.assessment')]", + config_parameter="spp_disability_registry.approval_definition_id", + help="Approval workflow applied to disability assessments. Create it under " + "Approvals > Approval Definitions (with Model = Disability Assessment), then " + "select it here. Until one is selected, assessments cannot be submitted for " + "approval.", + ) + def get_values(self): res = super().get_values() # nosemgrep: odoo-sudo-without-context — standard Odoo pattern for system parameter access diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index df8337b33..140737158 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -7,12 +7,19 @@
    + + + + +
    + +