diff --git a/spp_disability_registry/README.rst b/spp_disability_registry/README.rst index d6ff6d50a..408cb994e 100644 --- a/spp_disability_registry/README.rst +++ b/spp_disability_registry/README.rst @@ -40,7 +40,8 @@ Key Features scheduling - Impairment type, cause, and severity classifications using DCI vocabularies -- Assistive device management with status (needed, requested, provided) +- Assistive device management with status workflow (needed, requested, + provided) - Proxy response tracking for children - CEL function integration for program eligibility targeting diff --git a/spp_disability_registry/__manifest__.py b/spp_disability_registry/__manifest__.py index 9b83cc3ad..ea1779c54 100644 --- a/spp_disability_registry/__manifest__.py +++ b/spp_disability_registry/__manifest__.py @@ -31,7 +31,6 @@ "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/data/vocabulary_device.xml b/spp_disability_registry/data/vocabulary_device.xml index c240f8f7d..c1e355af0 100644 --- a/spp_disability_registry/data/vocabulary_device.xml +++ b/spp_disability_registry/data/vocabulary_device.xml @@ -2,12 +2,15 @@ Assistive Device Type urn:dci:cd:dr:04 2024 - Types of assistive devices + Types of assistive devices aligned with ISO 9999 classification True disability diff --git a/spp_disability_registry/demo/demo.xml b/spp_disability_registry/demo/demo.xml index 25f018a8b..0ba91aa49 100644 --- a/spp_disability_registry/demo/demo.xml +++ b/spp_disability_registry/demo/demo.xml @@ -39,20 +39,10 @@ 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 c92b55a5a..dd404f708 100644 --- a/spp_disability_registry/models/__init__.py +++ b/spp_disability_registry/models/__init__.py @@ -1,6 +1,4 @@ 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 bab1610e3..ff117d8e9 100644 --- a/spp_disability_registry/models/assessment.py +++ b/spp_disability_registry/models/assessment.py @@ -1,10 +1,9 @@ import logging from dateutil.relativedelta import relativedelta -from markupsafe import Markup from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -19,54 +18,6 @@ # 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"), -] -# Same scale without "No difficulty" — used by the "without his/her equipment or -# assistance" walking questions, where "no difficulty" unaided wouldn't warrant -# the aid in the first place (BM reword). -CFM_DIFFICULTY_LEVELS_NO_NONE = [ - ("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"), -] -# 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"), -] -# Yes/No gate questions in the CFM instruments (glasses, hearing aid, walking aid). -CFM_YES_NO = [ - ("yes", "Yes"), - ("no", "No"), -] - # Review category to months mapping REVIEW_CATEGORY_MONTHS = { "mie": 12, # Medical Improvement Expected: 6-18 months (using 12) @@ -126,30 +77,6 @@ class SppDisabilityAssessment(models.Model): compute="_compute_age_at_assessment", store=True, ) - # Surfaced so the form can hide "Age at Assessment" when there is no date of birth. - registrant_birthdate = fields.Date( - related="registrant_id.birthdate", - string="Registrant Date of Birth", - ) - 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.", - ) - 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.", - ) - questionnaire_complete = fields.Boolean( - string="Questionnaire Complete", - compute="_compute_questionnaire_complete", - help="Technical flag: whether the WG/CFM questionnaire has enough answers to " - "compute a disability result for the assessment type. Required to submit.", - ) # === WG-SS Responses (6 domains) === wg_seeing = fields.Selection( @@ -180,275 +107,30 @@ class SppDisabilityAssessment(models.Model): wg_communicating = fields.Selection( WG_DIFFICULTY_LEVELS, string="Communicating", - help="Do you have difficulty communicating (understanding or being understood) in their usual language?", - ) - - # === CFM 2-4 Responses (children aged 2-4, CF1-CF16) === - # Vision - cfm24_glasses = fields.Selection( # CF1 - CFM_YES_NO, - string="Does the child wear glasses?", - ) - cfm24_vision_aided = fields.Selection( # CF2 (asked when glasses are worn) - CFM_DIFFICULTY_LEVELS, - string="When wearing his/her glasses, does the child have difficulty seeing?", - ) - cfm24_vision = fields.Selection( # CF3 (asked when no glasses) - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty seeing?", - ) - # Hearing - cfm24_hearing_aid = fields.Selection( # CF4 - CFM_YES_NO, - string="Does the child use a hearing aid?", - ) - cfm24_hearing_aided = fields.Selection( # CF5 - CFM_DIFFICULTY_LEVELS, - string="When using his/her hearing aid, does the child have difficulty hearing sounds " - "like people's voices or music?", - ) - cfm24_hearing = fields.Selection( # CF6 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty hearing sounds like people's voices or music?", - ) - # Mobility - cfm24_walk_equipment = fields.Selection( # CF7 - CFM_YES_NO, - string="Does the child use any equipment or receive assistance for walking?", - ) - cfm24_walk_unaided = fields.Selection( # CF8 (without equipment) - CFM_DIFFICULTY_LEVELS_NO_NONE, - string="Without his/her equipment or assistance, does the child have difficulty walking?", - ) - cfm24_walk_aided = fields.Selection( # CF9 (with equipment) - concluding when equipment used - CFM_DIFFICULTY_LEVELS, - string="With his/her equipment or assistance, does the child have difficulty walking?", - ) - cfm24_walk_compare = fields.Selection( # CF10 (no equipment) - concluding when no equipment - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty walking?", - ) - # Dexterity - cfm24_dexterity = fields.Selection( # CF11 - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty picking " - "up small objects with his/her hand?", - ) - # Communication - cfm24_understand_you = fields.Selection( # CF12 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty understanding you?", - ) - cfm24_understood = fields.Selection( # CF13 - CFM_DIFFICULTY_LEVELS, - string="When the child speaks, do you have difficulty understanding him/her?", - ) - # Learning - cfm24_learning = fields.Selection( # CF14 - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty learning things?", - ) - # Playing - cfm24_playing = fields.Selection( # CF15 - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty playing?", - ) - # Controlling behaviour - cfm24_behavior = fields.Selection( # CF16 (behaviour scale, threshold "a lot more") - CFM_BEHAVIOR_LEVELS, - string="Compared with children of the same age, how much does the child kick, bite or hit " - "other children or adults?", + help="Do you have difficulty communicating (understanding or being understood)?", ) - # === CFM 5-17 Responses (children aged 5-17, CF1-CF24) === - # Vision - cfm517_glasses = fields.Selection( # CF1 - CFM_YES_NO, - string="Does the child wear glasses?", - ) - cfm517_vision_aided = fields.Selection( # CF2 - CFM_DIFFICULTY_LEVELS, - string="When wearing his/her glasses, does the child have difficulty seeing?", - ) - cfm517_vision = fields.Selection( # CF3 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty seeing?", - ) - # Hearing - cfm517_hearing_aid = fields.Selection( # CF4 - CFM_YES_NO, - string="Does the child use a hearing aid?", - ) - cfm517_hearing_aided = fields.Selection( # CF5 - CFM_DIFFICULTY_LEVELS, - string="When using his/her hearing aid, does the child have difficulty hearing sounds " - "like people's voices or music?", - ) - cfm517_hearing = fields.Selection( # CF6 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty hearing sounds like people's voices or music?", - ) - # Mobility (100/500 yards-meters, with and without equipment; concluding = the - # "with aid" answer when equipment is used, otherwise the "compared" answer) - cfm517_walk_equipment = fields.Selection( # CF7 - CFM_YES_NO, - string="Does the child use any equipment or receive assistance for walking?", - ) - cfm517_walk_unaided_100 = fields.Selection( # CF8 (without equipment, 100) - CFM_DIFFICULTY_LEVELS_NO_NONE, - string="Without his/her equipment or assistance, does the child have difficulty walking " - "100 yards/meters on level ground?", - ) - cfm517_walk_unaided_500 = fields.Selection( # CF9 (without equipment, 500) - CFM_DIFFICULTY_LEVELS_NO_NONE, - string="Without his/her equipment or assistance, does the child have difficulty walking " - "500 yards/meters on level ground?", - ) - cfm517_walk_aided_100 = fields.Selection( # CF10 (with equipment, 100) - concluding - CFM_DIFFICULTY_LEVELS, - string="With his/her equipment or assistance, does the child have difficulty walking " - "100 yards/meters on level ground?", - ) - cfm517_walk_aided_500 = fields.Selection( # CF11 (with equipment, 500) - concluding - CFM_DIFFICULTY_LEVELS, - string="With his/her equipment or assistance, does the child have difficulty walking " - "500 yards/meters on level ground?", - ) - cfm517_walk_compare_100 = fields.Selection( # CF12 (no equipment, compared, 100) - concluding - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty walking " - "100 yards/meters on level ground?", - ) - cfm517_walk_compare_500 = fields.Selection( # CF13 (no equipment, compared, 500) - concluding - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty walking " - "500 yards/meters on level ground?", - ) - # Self-care - cfm517_selfcare = fields.Selection( # CF14 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty with self-care such as feeding or dressing him/herself?", - ) - # Communication - cfm517_comm_inside = fields.Selection( # CF15 - CFM_DIFFICULTY_LEVELS, - string="When the child speaks, does he/she have difficulty being understood by people inside this household?", - ) - cfm517_comm_outside = fields.Selection( # CF16 - CFM_DIFFICULTY_LEVELS, - string="When the child speaks, does he/she have difficulty being understood by people outside this household?", - ) - # Learning - cfm517_learning = fields.Selection( # CF17 - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty learning things?", - ) - # Remembering - cfm517_remembering = fields.Selection( # CF18 - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty remembering things?", - ) - # Concentrating - cfm517_concentrating = fields.Selection( # CF19 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty concentrating on an activity that he/she enjoys doing?", - ) - # Accepting change - cfm517_accepting_change = fields.Selection( # CF20 - CFM_DIFFICULTY_LEVELS, - string="Does the child have difficulty accepting changes in his/her routine?", - ) - # Controlling behaviour (standard difficulty scale in CFM 5-17) - cfm517_behavior = fields.Selection( # CF21 - CFM_DIFFICULTY_LEVELS, - string="Compared with children of the same age, does the child have difficulty controlling his/her behaviour?", - ) - # Making friends - cfm517_friends = fields.Selection( # CF22 - CFM_DIFFICULTY_LEVELS, - string="Does the child have 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 vocabularies) === - # Gate question (OP#1068): "No" marks the tab complete with no lines; - # "Yes" reveals the list and requires at least one row before submission. - has_impairments_to_record = fields.Selection( - [("yes", "Yes"), ("no", "No")], - string="Does the registrant have any impairments or functional limitations to record?", - help="If Yes, classify each impairment below. If No, this tab is considered complete.", - ) - # 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 Classification (DCI DO.DR.02) === impairment_type_ids = fields.Many2many( "spp.vocabulary.code", "spp_disability_assessment_impairment_type_rel", "assessment_id", "code_id", string="Impairment Types", - compute="_compute_impairment_summary", - store=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", - compute="_compute_impairment_summary", - store=True, + domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:02')]", tracking=True, - help="Overall severity: the most severe level across the impairment classification lines.", - ) - # Per-line "Type — Severity" rendering for the registrant overview table (OP#1068): - # each impairment type shows its own severity on a separate line. - impairment_severity_display = fields.Html( - string="Impairments", - compute="_compute_impairment_severity_display", - help="Each classified impairment type with its severity, one per line.", ) - @api.depends( - "impairment_line_ids.impairment_type_id", - "impairment_line_ids.severity_level_id", - ) - def _compute_impairment_severity_display(self): - for rec in self: - rows = [] - for line in rec.impairment_line_ids: - label = line.impairment_type_id.display or line.impairment_type_id.code or "" - severity = line.severity_level_id.display or line.severity_level_id.code or "" - text = f"{label} — {severity}" if severity else label - if text: - rows.append(Markup("
%s
") % text) - rec.impairment_severity_display = Markup("").join(rows) if rows else False - - @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( [ @@ -471,117 +153,16 @@ def _compute_impairment_summary(self): string="Support Needs", help="Free-text description of support needs. Countries can extend with structured fields.", ) - # Assistive-device requests (OP#1068). On approval each becomes a real - # spp.assistive.device (status "needed") on the registrant. - device_request_ids = fields.One2many( - "spp.disability.assessment.device.request", - "assessment_id", - string="Assistive Device Requests", - ) - - # === Assessment-tab configuration mirrors (OP#1068) === - # Read from ir.config_parameter so the form (tab visibility) and the submit - # gate can react to the Disability Registry settings. - cfg_display_impairment = fields.Boolean(compute="_compute_tab_config") - cfg_display_wg = fields.Boolean(compute="_compute_tab_config") - cfg_display_support = fields.Boolean(compute="_compute_tab_config") - cfg_require_impairment = fields.Boolean(compute="_compute_tab_config") - cfg_require_wg = fields.Boolean(compute="_compute_tab_config") - cfg_require_support = fields.Boolean(compute="_compute_tab_config") - cfg_support_show_devices = fields.Boolean(compute="_compute_tab_config") - cfg_display_review = fields.Boolean(compute="_compute_tab_config") - cfg_require_review = fields.Boolean(compute="_compute_tab_config") - cfg_require_proxy_details = fields.Boolean(compute="_compute_tab_config") - - @api.depends_context("uid") - def _compute_tab_config(self): - # nosemgrep: odoo-sudo-without-context — read configuration parameters - icp = self.env["ir.config_parameter"].sudo() - - def flag(key): - return icp.get_param("spp_disability_registry." + key, "True") == "True" - - values = { - "cfg_display_impairment": flag("display_impairment"), - "cfg_display_wg": flag("display_wg"), - "cfg_display_support": flag("display_support"), - "cfg_require_impairment": flag("require_impairment"), - "cfg_require_wg": flag("require_wg"), - "cfg_require_support": flag("require_support"), - "cfg_support_show_devices": flag("support_show_devices"), - "cfg_display_review": flag("display_review"), - "cfg_require_review": flag("require_review"), - "cfg_require_proxy_details": flag("require_proxy_details"), - } - for rec in self: - rec.update(values) - - # === Submission completeness (OP#1068) === - impairment_tab_complete = fields.Boolean( - compute="_compute_assessment_complete", - help="Impairment tab complete: 'No' to the gate question, or 'Yes' with at least one row.", - ) - assessment_complete = fields.Boolean( - compute="_compute_assessment_complete", - help="True when every displayed and required assessment tab is complete.", - ) - - @api.depends( - "has_impairments_to_record", - "impairment_line_ids", - "questionnaire_complete", - "review_category", - "is_proxy_response", - "proxy_respondent_id", - "proxy_relationship", - "cfg_display_impairment", - "cfg_display_wg", - "cfg_display_review", - "cfg_require_impairment", - "cfg_require_wg", - "cfg_require_review", - "cfg_require_proxy_details", - ) - def _compute_assessment_complete(self): - for rec in self: - # Impairment tab: "No" → complete; "Yes" → at least one classified row. - rec.impairment_tab_complete = rec.has_impairments_to_record == "no" or ( - rec.has_impairments_to_record == "yes" and bool(rec.impairment_line_ids) - ) - checks = [] - if rec.cfg_display_impairment and rec.cfg_require_impairment: - checks.append(rec.impairment_tab_complete) - if rec.cfg_display_wg and rec.cfg_require_wg: - checks.append(rec.questionnaire_complete) - # Review schedule: when shown and required, a review category must be set (OP#1068). - if rec.cfg_display_review and rec.cfg_require_review: - checks.append(bool(rec.review_category)) - # Proxy details: when a proxy responded and details are required, the - # respondent and their relationship must be recorded (OP#1053). - if rec.cfg_require_proxy_details and rec.is_proxy_response: - checks.append(bool(rec.proxy_respondent_id) and bool(rec.proxy_relationship)) - # Support Needs has no content gate (OP#1068), so it never blocks. - rec.assessment_complete = all(checks) # === Proxy Response Tracking === is_proxy_response = fields.Boolean( string="Proxy Response", - 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).", + default=False, + help="True if responses provided by proxy (always true for children)", ) proxy_respondent_id = fields.Many2one( "res.partner", string="Proxy Respondent", - domain="[('id', '!=', registrant_id)]", help="Person who provided responses on behalf of the registrant", ) proxy_relationship = fields.Selection( @@ -594,19 +175,6 @@ def _compute_assessment_complete(self): ], 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( @@ -659,300 +227,18 @@ 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_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() - 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.""" - 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 - - 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() - - def _questionnaire_answers(self): - """The answers that feed the disability result for this assessment type - (the concluding answer per branched domain). The questionnaire is - 'complete enough to compute a result' when all of these are answered. - """ - self.ensure_one() - if self.assessment_type == "cfm_2_4": - vision = self.cfm24_vision_aided if self.cfm24_glasses == "yes" else self.cfm24_vision - hearing = self.cfm24_hearing_aided if self.cfm24_hearing_aid == "yes" else self.cfm24_hearing - mobility = self.cfm24_walk_aided if self.cfm24_walk_equipment == "yes" else self.cfm24_walk_compare - return [ - vision, - hearing, - mobility, - self.cfm24_dexterity, - self.cfm24_understand_you, - self.cfm24_understood, - self.cfm24_learning, - self.cfm24_playing, - self.cfm24_behavior, - ] - if self.assessment_type == "cfm_5_17": - vision = self.cfm517_vision_aided if self.cfm517_glasses == "yes" else self.cfm517_vision - hearing = self.cfm517_hearing_aided if self.cfm517_hearing_aid == "yes" else self.cfm517_hearing - if self.cfm517_walk_equipment == "yes": - mobility = [self.cfm517_walk_aided_100, self.cfm517_walk_aided_500] - else: - mobility = [self.cfm517_walk_compare_100, self.cfm517_walk_compare_500] - return [ - vision, - hearing, - *mobility, - 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, - self.cfm517_anxiety, - self.cfm517_depression, - ] - # WG-SS (adult) and any fallback. - return [ - self.wg_seeing, - self.wg_hearing, - self.wg_walking, - self.wg_remembering, - self.wg_selfcare, - self.wg_communicating, - ] - - @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", - "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_questionnaire_complete(self): - for rec in self: - answers = rec._questionnaire_answers() - rec.questionnaire_complete = bool(answers) and all(answers) - - def _check_can_submit(self): - """Require every displayed + required tab to be complete before submitting.""" - super()._check_can_submit() - if not self.assessment_complete: - raise UserError( - _( - "Complete all required parts of the assessment before submitting for " - "approval: the impairment question, the WG/CFM questionnaire, the review " - "schedule, and - when a proxy responded - the proxy respondent and " - "relationship (whichever are required in the Disability Registry settings)." - ) - ) - - def _sync_registrant_disability_status(self): - """Notify the ORM that approval_state changed so the registrant's - disability status (current_disability_assessment_id, has_disability, ...) - recomputes. The approval mixin writes approval_state via raw SQL for - optimistic locking, which bypasses ORM dependency tracking, so the - registrant would otherwise show a stale status (#1022). - """ - self.modified(["approval_state"]) - - def _on_approve(self): - res = super()._on_approve() - self._sync_registrant_disability_status() - self._materialize_device_requests() - return res - - def _materialize_device_requests(self): - """On approval, turn each Support-Needs device request into a real - spp.assistive.device (status 'needed') on the registrant so it shows on - the Overview / registrant (OP#1068). Skips a device type that already - has a pending 'needed' record to avoid duplicates on re-approval. - """ - Device = self.env["spp.assistive.device"] - for rec in self: - for req in rec.device_request_ids: - already = Device.search_count( - [ - ("registrant_id", "=", rec.registrant_id.id), - ("device_type_id", "=", req.device_type_id.id), - ("status", "=", "needed"), - ] - ) - if not already: - Device.create( - { - "registrant_id": rec.registrant_id.id, - "device_type_id": req.device_type_id.id, - "status": "needed", - } - ) - - def _on_reject(self, reason): - res = super()._on_reject(reason) - self._sync_registrant_disability_status() - return res - - def _on_reset_to_draft(self): - res = super()._on_reset_to_draft() - self._sync_registrant_disability_status() - return res - @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", "assessment_date") - def _check_birthdate_required_for_age(self): - """When the assessment type is age-driven, require a date of birth and an - age of at least 2 (the youngest CFM instrument covers ages 2-4).""" - if self._disability_disregard_age(): - return for rec in self: - if not rec.registrant_id: - continue - if 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, - ) - ) - if rec.age_at_assessment < 2: - raise ValidationError( - _( - "Disability assessments are only available for individuals aged 2 years or " - "older. %s is younger than 2 at the assessment date.", - rec.registrant_id.display_name, - ) - ) + 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" @api.depends("review_category", "assessment_date") def _compute_next_review_date(self): @@ -963,144 +249,36 @@ 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 (gate answered "yes" = aided path). - vision = self.cfm24_vision_aided if self.cfm24_glasses == "yes" else self.cfm24_vision - hearing = self.cfm24_hearing_aided if self.cfm24_hearing_aid == "yes" else self.cfm24_hearing - mobility = self.cfm24_walk_aided if self.cfm24_walk_equipment == "yes" 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 - - 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 == "yes" else self.cfm517_vision - hearing = self.cfm517_hearing_aided if self.cfm517_hearing_aid == "yes" else self.cfm517_hearing - if self.cfm517_walk_equipment == "yes": - 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", "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", - "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. - - Any domain at/above threshold marks the person as having a disability. - """ + """WG standard: any domain with 'a_lot' or 'cannot' indicates disability.""" 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: - domain_count = rec._wg_ss_domain_count() + 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) 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/assistive_device.py b/spp_disability_registry/models/assistive_device.py index 691e1bf67..27cf1b017 100644 --- a/spp_disability_registry/models/assistive_device.py +++ b/spp_disability_registry/models/assistive_device.py @@ -77,38 +77,3 @@ def action_view_registrant(self): "view_mode": "form", "target": "current", } - - -class SppDisabilityAssessmentDeviceRequest(models.Model): - """Assistive-device request captured on an assessment's Support Needs tab. - - Status is always "Needed" (read-only). On assessment approval each row - creates a real ``spp.assistive.device`` (status "needed") on the registrant, - so it surfaces on the Overview / registrant (OP#1068). - """ - - _name = "spp.disability.assessment.device.request" - _description = "Assessment Assistive Device Request" - - assessment_id = fields.Many2one( - "spp.disability.assessment", - string="Assessment", - required=True, - index=True, - ondelete="cascade", - ) - device_type_id = fields.Many2one( - "spp.vocabulary.code", - string="Device Type", - required=True, - domain="[('vocabulary_id.namespace_uri', '=', 'urn:dci:cd:dr:04')]", - ) - # Always "Needed" — these are requests; the registrant's device record is - # what later moves to requested/provided. - status = fields.Selection( - [("needed", "Needed")], - string="Status", - default="needed", - readonly=True, - ) - notes = fields.Text(string="Notes") diff --git a/spp_disability_registry/models/impairment.py b/spp_disability_registry/models/impairment.py deleted file mode 100644 index f60a5d633..000000000 --- a/spp_disability_registry/models/impairment.py +++ /dev/null @@ -1,38 +0,0 @@ -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/models/registrant.py b/spp_disability_registry/models/registrant.py index 5ceb36635..7686b28c9 100644 --- a/spp_disability_registry/models/registrant.py +++ b/spp_disability_registry/models/registrant.py @@ -1,8 +1,6 @@ import logging -from dateutil.relativedelta import relativedelta - -from odoo import _, api, fields, models +from odoo import api, fields, models _logger = logging.getLogger(__name__) @@ -69,38 +67,6 @@ class ResPartner(models.Model): store=True, ) - # === Assessment Eligibility === - can_create_disability_assessment = fields.Boolean( - string="Can Create Disability Assessment", - compute="_compute_can_create_disability_assessment", - ) - disability_no_create_reason = fields.Char( - compute="_compute_can_create_disability_assessment", - ) - - @api.depends("birthdate") - def _compute_can_create_disability_assessment(self): - # When the assessment type is age-driven, an assessment needs a date of birth - # and an age of at least 2 (the youngest CFM instrument covers ages 2-4). - # nosemgrep: odoo-sudo-without-context — read configuration parameter - icp = self.env["ir.config_parameter"].sudo() - disregard_age = icp.get_param("spp_disability_registry.disregard_age", "False") == "True" - today = fields.Date.today() - for rec in self: - reason = False - if disregard_age: - can_create = True - elif not rec.birthdate: - can_create = False - reason = _("Add the registrant's date of birth to create a disability assessment.") - elif relativedelta(today, rec.birthdate).years < 2: - can_create = False - reason = _("Disability assessments are not available for children under 2 years old.") - else: - can_create = True - rec.can_create_disability_assessment = can_create - rec.disability_no_create_reason = reason - def _compute_disability_assessment_count(self): for rec in self: rec.disability_assessment_count = len(rec.disability_assessment_ids) diff --git a/spp_disability_registry/models/res_config_settings.py b/spp_disability_registry/models/res_config_settings.py deleted file mode 100644 index da35f439f..000000000 --- a/spp_disability_registry/models/res_config_settings.py +++ /dev/null @@ -1,157 +0,0 @@ -from odoo import _, fields, models -from odoo.exceptions import ValidationError - - -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, the 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).", - ) - # Char (not Integer) + managed manually so it is genuinely blank by default - # rather than showing "0" or a stale value (BM reword). Stored as a plain - # number string; readers parse it with int(value or 0). - disability_self_report_min_age = fields.Char( - string="Minimum age for self-report (CFM 5-17)", - help="Minimum age at assessment at which self-report is allowed on CFM 5-17. " - "Required when self-report is enabled; must be between 5 and 17. Blank by default.", - ) - # 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, the proxy response flag can be ticked on adult WG-SS " - "assessments. When disabled, WG-SS assessments are self-report only.", - ) - # When enabled, recording who responded is required before an assessment that - # uses a proxy response can be submitted for approval (OP#1053). - disability_require_proxy_details = fields.Boolean( - string="Require proxy details on proxy responses", - default=True, - help="When enabled, an assessment answered by a proxy cannot be submitted " - "until the proxy respondent and relationship are recorded.", - ) - - # === 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.", - ) - - # === Assessment Tabs (OP#1068) === - # Which of the three assessment tabs are displayed, and which are required - # before an assessment can be submitted for approval. All default to True. - disability_display_impairment = fields.Boolean( - string="Show Impairment Classification tab", - default=True, - ) - disability_display_wg = fields.Boolean( - string="Show WG/CFM Assessment tab", - default=True, - ) - disability_display_support = fields.Boolean( - string="Show Support Needs tab", - default=True, - ) - disability_require_impairment = fields.Boolean( - string="Require Impairment Classification to submit", - default=True, - ) - disability_require_wg = fields.Boolean( - string="Require WG/CFM Assessment to submit", - default=True, - ) - disability_require_support = fields.Boolean( - string="Require Support Needs to submit", - default=True, - ) - disability_support_show_devices = fields.Boolean( - string="Show Assistive Devices on Support Needs", - default=True, - ) - disability_display_review = fields.Boolean( - string="Show Review Schedule", - default=True, - ) - disability_require_review = fields.Boolean( - string="Require Review Schedule to submit", - default=True, - ) - - # field name -> ir.config_parameter key, for default-True booleans that must - # round-trip a False value (a config_parameter boolean cannot — see the note - # on disability_allow_proxy_wg_ss above). - _DEFAULT_TRUE_PARAMS = { - "disability_allow_proxy_wg_ss": "spp_disability_registry.allow_proxy_wg_ss", - "disability_require_proxy_details": "spp_disability_registry.require_proxy_details", - "disability_display_impairment": "spp_disability_registry.display_impairment", - "disability_display_wg": "spp_disability_registry.display_wg", - "disability_display_support": "spp_disability_registry.display_support", - "disability_display_review": "spp_disability_registry.display_review", - "disability_require_impairment": "spp_disability_registry.require_impairment", - "disability_require_wg": "spp_disability_registry.require_wg", - "disability_require_support": "spp_disability_registry.require_support", - "disability_require_review": "spp_disability_registry.require_review", - } - - 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() - for field_name, key in self._DEFAULT_TRUE_PARAMS.items(): - res[field_name] = icp.get_param(key, "True") == "True" - # Blank by default (never "0"); stored only when self-report is enabled. - res["disability_self_report_min_age"] = icp.get_param("spp_disability_registry.self_report_min_age", "") - return res - - def set_values(self): - # When self-report on CFM 5-17 is enabled, a minimum age between 5 and 17 - # must be provided. - min_age = 0 - if self.disability_allow_self_report_cfm: - try: - min_age = int(self.disability_self_report_min_age or 0) - except (TypeError, ValueError): - min_age = 0 - if not (5 <= min_age <= 17): - raise ValidationError( - _("Enter a minimum self-report age between 5 and 17 to allow self-report on CFM 5-17.") - ) - super().set_values() - # nosemgrep: odoo-sudo-without-context — standard Odoo pattern for system parameter access - icp = self.env["ir.config_parameter"].sudo() - for field_name, key in self._DEFAULT_TRUE_PARAMS.items(): - icp.set_param(key, "True" if self[field_name] else "False") - # Store the validated age only while self-report is on; otherwise clear it - # so the field is blank by default next time. - icp.set_param( - "spp_disability_registry.self_report_min_age", - str(min_age) if self.disability_allow_self_report_cfm else "", - ) diff --git a/spp_disability_registry/readme/DESCRIPTION.md b/spp_disability_registry/readme/DESCRIPTION.md index 3ff8a72f5..4e209323f 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 (needed, requested, provided) +- Assistive device management with status workflow (needed, requested, provided) - Proxy response tracking for children - CEL function integration for program eligibility targeting diff --git a/spp_disability_registry/security/ir.model.access.csv b/spp_disability_registry/security/ir.model.access.csv index 48a34a4f0..c2ca0a345 100644 --- a/spp_disability_registry/security/ir.model.access.csv +++ b/spp_disability_registry/security/ir.model.access.csv @@ -7,11 +7,3 @@ 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 -access_spp_disability_assessment_device_request_viewer,spp.disability.assessment.device.request viewer,model_spp_disability_assessment_device_request,group_disability_viewer,1,0,0,0 -access_spp_disability_assessment_device_request_assessor,spp.disability.assessment.device.request assessor,model_spp_disability_assessment_device_request,group_disability_assessor,1,1,1,1 -access_spp_disability_assessment_device_request_validator,spp.disability.assessment.device.request validator,model_spp_disability_assessment_device_request,group_disability_validator,1,1,1,1 -access_spp_disability_assessment_device_request_manager,spp.disability.assessment.device.request manager,model_spp_disability_assessment_device_request,group_disability_manager,1,1,1,1 diff --git a/spp_disability_registry/static/description/index.html b/spp_disability_registry/static/description/index.html index b2538da1f..27db6080d 100644 --- a/spp_disability_registry/static/description/index.html +++ b/spp_disability_registry/static/description/index.html @@ -387,7 +387,8 @@

Key Features

scheduling
  • Impairment type, cause, and severity classifications using DCI vocabularies
  • -
  • Assistive device management with status (needed, requested, provided)
  • +
  • Assistive device management with status workflow (needed, requested, +provided)
  • Proxy response tracking for children
  • CEL function integration for program eligibility targeting
  • diff --git a/spp_disability_registry/tests/__init__.py b/spp_disability_registry/tests/__init__.py index 05aa9e197..c095b34b6 100644 --- a/spp_disability_registry/tests/__init__.py +++ b/spp_disability_registry/tests/__init__.py @@ -1,4 +1,3 @@ from . import test_assessment from . import test_cel_functions -from . import test_coverage_gaps from . import test_registrant diff --git a/spp_disability_registry/tests/test_assessment.py b/spp_disability_registry/tests/test_assessment.py index c46f0daf2..e1b8fa290 100644 --- a/spp_disability_registry/tests/test_assessment.py +++ b/spp_disability_registry/tests/test_assessment.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta from odoo import Command -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import ValidationError from odoo.tests import TransactionCase, tagged @@ -281,214 +281,17 @@ def test_assessment_name_computed(self): # === Proxy Response Tests === def test_proxy_flag_set_for_child(self): - """Proxy response flag is set automatically for child (CFM) assessments.""" + """Test that proxy response flag is set for child assessments.""" assessment = self.env["spp.disability.assessment"].create( { "registrant_id": self.child_registrant.id, "assessment_date": date.today(), } ) - # is_proxy_response is computed from the (age-derived) assessment type. - self.assertIn(assessment.assessment_type, ("cfm_2_4", "cfm_5_17")) + # Trigger onchange + assessment._onchange_assessment_type() self.assertTrue(assessment.is_proxy_response) - def test_questionnaire_required_before_submit(self): - """A blank questionnaire blocks submission; completing it lifts the gate.""" - assessment = self.env["spp.disability.assessment"].create( - { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - } - ) - # WG-SS with no answers cannot be submitted. - self.assertFalse(assessment.questionnaire_complete) - with self.assertRaises(UserError): - assessment.action_submit_for_approval() - # Answering all six WG-SS domains makes it complete (even "no difficulty"). - assessment.write( - { - "wg_seeing": "none", - "wg_hearing": "none", - "wg_walking": "none", - "wg_remembering": "none", - "wg_selfcare": "none", - "wg_communicating": "none", - } - ) - self.assertTrue(assessment.questionnaire_complete) - - def test_approval_propagates_to_registrant(self): - """Approving updates the registrant's disability status (#1022). - - The approval mixin writes approval_state via raw SQL, so the registrant's - computed status must be re-synced via the _on_approve hook. - """ - assessment = self.env["spp.disability.assessment"].create( - { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - "wg_seeing": "cannot", - "wg_hearing": "none", - "wg_walking": "none", - "wg_remembering": "none", - "wg_selfcare": "none", - "wg_communicating": "none", - } - ) - self.assertTrue(assessment.has_disability) - self.assertFalse(self.adult_registrant.has_disability) - # Simulate a submitted record and approve via the mixin's SQL path. - assessment.write({"approval_state": "pending"}) - assessment._do_approve() - self.assertEqual(assessment.approval_state, "approved") - self.assertTrue(self.adult_registrant.has_disability) - self.assertEqual(self.adult_registrant.current_disability_assessment_id, assessment) - - # === OP#1068: tab config, impairment gate, device requests === - def _full_wg(self): - return { - "wg_seeing": "none", - "wg_hearing": "none", - "wg_walking": "none", - "wg_remembering": "none", - "wg_selfcare": "none", - "wg_communicating": "none", - } - - def test_assessment_complete_requires_impairment_answer(self): - """With the default config (impairment + WG required), a complete - questionnaire alone isn't enough — the impairment question must be - answered (OP#1068).""" - vals = {"registrant_id": self.adult_registrant.id, "assessment_date": date.today()} - vals.update(self._full_wg()) - a = self.env["spp.disability.assessment"].create(vals) - # Review schedule is required by default (OP#1068); set it so this test - # isolates the impairment-answer gate. - a.review_category = "mie" - self.assertTrue(a.questionnaire_complete) - self.assertFalse(a.assessment_complete) # impairment question unanswered - # "No" → impairment tab complete → assessment complete (support never gates). - a.has_impairments_to_record = "no" - self.assertTrue(a.impairment_tab_complete) - self.assertTrue(a.assessment_complete) - # "Yes" with no rows → incomplete again. - a.has_impairments_to_record = "yes" - self.assertFalse(a.impairment_tab_complete) - self.assertFalse(a.assessment_complete) - - def test_gate_is_config_driven(self): - """When WG/CFM is configured as not required, a blank questionnaire no - longer blocks completion (OP#1068).""" - icp = self.env["ir.config_parameter"].sudo() - icp.set_param("spp_disability_registry.require_wg", "False") - a = self.env["spp.disability.assessment"].create( - { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - "has_impairments_to_record": "no", - # Review schedule is required by default (OP#1068); set it so the - # WG config gate is the only variable under test. - "review_category": "mie", - } - ) - a.invalidate_recordset() - self.assertFalse(a.questionnaire_complete) - self.assertFalse(a.cfg_require_wg) - self.assertTrue(a.assessment_complete) - - def test_review_schedule_gates_submission(self): - """Review schedule is required by default (OP#1068): a complete WG + - impairment answer is not enough until a review category is set, and - disabling the requirement removes the gate.""" - vals = { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - "has_impairments_to_record": "no", - } - vals.update(self._full_wg()) - a = self.env["spp.disability.assessment"].create(vals) - # impairment + WG satisfied, but review category missing -> blocked - self.assertFalse(a.assessment_complete) - a.review_category = "mie" - self.assertTrue(a.assessment_complete) - # When review is not required, a missing category no longer blocks. - a.review_category = False - self.env["ir.config_parameter"].sudo().set_param("spp_disability_registry.require_review", "False") - a.invalidate_recordset() - self.assertFalse(a.cfg_require_review) - self.assertTrue(a.assessment_complete) - - def test_proxy_details_required_when_proxy(self): - """When a proxy responded and proxy details are required (default), - the assessment cannot be completed until the respondent and the - relationship are recorded (OP#1053).""" - vals = { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - "has_impairments_to_record": "no", - "review_category": "mie", - } - vals.update(self._full_wg()) - a = self.env["spp.disability.assessment"].create(vals) - self.assertTrue(a.assessment_complete) # baseline complete, no proxy in use - # Mark as a proxy response -> proxy details now required to complete. - a.is_proxy_response = True - self.assertTrue(a.cfg_require_proxy_details) - self.assertFalse(a.assessment_complete) - a.proxy_respondent_id = self.child_registrant.id - a.proxy_relationship = "parent" - self.assertTrue(a.assessment_complete) - - def test_impairment_severity_display(self): - """Each impairment line renders its type with its own severity on a - separate line in the overview display (OP#1068).""" - imp_types = self.env["spp.vocabulary.code"].search( - [("vocabulary_id.namespace_uri", "=", "urn:dci:cd:dr:01")], limit=2 - ) - severities = self.env["spp.vocabulary.code"].search( - [("vocabulary_id.namespace_uri", "=", "urn:dci:cd:dr:02")], limit=2 - ) - self.assertTrue(len(imp_types) >= 2 and len(severities) >= 2, "need 2 impairment types + 2 severities") - a = self.env["spp.disability.assessment"].create( - { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - "has_impairments_to_record": "yes", - "impairment_line_ids": [ - (0, 0, {"impairment_type_id": imp_types[0].id, "severity_level_id": severities[0].id}), - (0, 0, {"impairment_type_id": imp_types[1].id, "severity_level_id": severities[1].id}), - ], - } - ) - html = str(a.impairment_severity_display or "") - for code in (imp_types[0], imp_types[1], severities[0], severities[1]): - self.assertIn(code.display, html) - # One
    per impairment line -> two separate lines. - self.assertEqual(html.count("
    "), 2) - - def test_device_requests_materialize_on_approve(self): - """Support-Needs device requests become spp.assistive.device (status - 'needed') on the registrant when the assessment is approved (OP#1068).""" - device_type = self.env["spp.vocabulary.code"].search( - [("vocabulary_id.namespace_uri", "=", "urn:dci:cd:dr:04")], limit=1 - ) - if not device_type: - self.skipTest("no assistive-device type vocabulary code present") - a = self.env["spp.disability.assessment"].create( - { - "registrant_id": self.adult_registrant.id, - "assessment_date": date.today(), - "device_request_ids": [(0, 0, {"device_type_id": device_type.id})], - } - ) - Device = self.env["spp.assistive.device"] - self.assertFalse(Device.search_count([("registrant_id", "=", self.adult_registrant.id)])) - a.write({"approval_state": "pending"}) - a._do_approve() - dev = Device.search([("registrant_id", "=", self.adult_registrant.id), ("device_type_id", "=", device_type.id)]) - self.assertEqual(len(dev), 1) - self.assertEqual(dev.status, "needed") - # === Date Validation Tests === def test_future_assessment_date_rejected(self): diff --git a/spp_disability_registry/tests/test_cel_functions.py b/spp_disability_registry/tests/test_cel_functions.py index 2cfc06fc3..c0541ae1a 100644 --- a/spp_disability_registry/tests/test_cel_functions.py +++ b/spp_disability_registry/tests/test_cel_functions.py @@ -95,11 +95,6 @@ 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( @@ -107,9 +102,7 @@ def setUpClass(cls): "registrant_id": cls.member_with_disability.id, "assessment_date": date.today(), "wg_walking": "a_lot", - "impairment_line_ids": [ - (0, 0, {"impairment_type_id": cls.impairment_type.id, "severity_level_id": cls.severity_mild.id}) - ], + "severity_level_id": cls.severity_mild.id if cls.severity_mild else False, } ) assessment1.write({"approval_state": "approved"}) @@ -121,9 +114,7 @@ def setUpClass(cls): "assessment_date": date.today(), "wg_seeing": "cannot", "wg_hearing": "cannot", - "impairment_line_ids": [ - (0, 0, {"impairment_type_id": cls.impairment_type.id, "severity_level_id": cls.severity_severe.id}) - ], + "severity_level_id": cls.severity_severe.id if cls.severity_severe else False, "review_category": "mine", } ) @@ -309,9 +300,7 @@ def test_needs_reassessment_true_when_due(self): "registrant_id": overdue_registrant.id, "assessment_date": date.today() - relativedelta(years=2), "wg_walking": "a_lot", - "impairment_line_ids": [ - (0, 0, {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_mild.id}) - ], + "severity_level_id": self.severity_mild.id if self.severity_mild else False, "review_category": "mie", # 12 months, so overdue } ) diff --git a/spp_disability_registry/tests/test_coverage_gaps.py b/spp_disability_registry/tests/test_coverage_gaps.py deleted file mode 100644 index 4da2d81ce..000000000 --- a/spp_disability_registry/tests/test_coverage_gaps.py +++ /dev/null @@ -1,145 +0,0 @@ -from datetime import date - -from dateutil.relativedelta import relativedelta - -from odoo.exceptions import ValidationError -from odoo.tests import TransactionCase, tagged - - -@tagged("post_install", "-at_install") -class TestDisabilityConfigAndModels(TransactionCase): - """Covers configuration settings, registrant eligibility/rollups, assistive - devices, impairment lines and assessment device requests.""" - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.code = cls.env["spp.vocabulary.code"] - cls.registrant = cls.env["res.partner"].create( - { - "name": "Coverage Adult", - "is_registrant": True, - "is_group": False, - "birthdate": date.today() - relativedelta(years=30), - } - ) - - def _code(self, namespace): - return self.code.search([("vocabulary_id.namespace_uri", "=", namespace)], limit=1) - - # === res.config.settings === - def test_config_settings_roundtrip(self): - Settings = self.env["res.config.settings"] - settings = Settings.create( - { - "disability_allow_proxy_wg_ss": False, - "disability_display_impairment": False, - "disability_require_support": False, - "disability_allow_self_report_cfm": True, - "disability_self_report_min_age": "10", - } - ) - settings.set_values() - - icp = self.env["ir.config_parameter"].sudo() - self.assertEqual(icp.get_param("spp_disability_registry.allow_proxy_wg_ss"), "False") - self.assertEqual(icp.get_param("spp_disability_registry.display_impairment"), "False") - self.assertEqual(icp.get_param("spp_disability_registry.self_report_min_age"), "10") - - vals = settings.get_values() - self.assertFalse(vals["disability_allow_proxy_wg_ss"]) - self.assertFalse(vals["disability_display_impairment"]) - self.assertTrue(vals["disability_require_wg"]) # untouched default stays True - self.assertEqual(vals["disability_self_report_min_age"], "10") - - def test_config_self_report_min_age_out_of_range(self): - Settings = self.env["res.config.settings"] - settings = Settings.create( - { - "disability_allow_self_report_cfm": True, - "disability_self_report_min_age": "3", - } - ) - with self.assertRaises(ValidationError): - settings.set_values() - - # === Registrant eligibility + actions === - def test_can_create_assessment_by_age(self): - # Adult with a birthdate can create. - self.assertTrue(self.registrant.can_create_disability_assessment) - - # No birthdate -> blocked, with a reason. - no_bd = self.env["res.partner"].create({"name": "No Birthdate", "is_registrant": True, "is_group": False}) - self.assertFalse(no_bd.can_create_disability_assessment) - self.assertTrue(no_bd.disability_no_create_reason) - - # Under 2 -> blocked. - baby = self.env["res.partner"].create( - { - "name": "Under Two", - "is_registrant": True, - "is_group": False, - "birthdate": date.today() - relativedelta(years=1), - } - ) - self.assertFalse(baby.can_create_disability_assessment) - - def test_registrant_action_helpers(self): - self.assertEqual(self.registrant.action_view_disability_assessments()["res_model"], "spp.disability.assessment") - self.assertEqual(self.registrant.action_view_assistive_devices()["res_model"], "spp.assistive.device") - self.assertEqual( - self.registrant.action_create_disability_assessment()["res_model"], "spp.disability.assessment" - ) - - # === Assistive device === - def test_assistive_device_name_onchange_and_rollup(self): - device_type = self._code("urn:dci:cd:dr:04") - if not device_type: - self.skipTest("no device-type vocabulary codes present") - device = self.env["spp.assistive.device"].create( - { - "registrant_id": self.registrant.id, - "device_type_id": device_type.id, - "status": "provided", - "provision_date": date.today(), - } - ) - self.assertIn(self.registrant.name, device.name) - self.assertIn(device_type.display, device.name) - - # Changing away from "provided" clears the provision date. - device.status = "needed" - device._onchange_status() - self.assertFalse(device.provision_date) - - action = device.action_view_registrant() - self.assertEqual(action["res_id"], self.registrant.id) - - self.registrant.invalidate_recordset() - self.assertEqual(self.registrant.assistive_device_count, 1) - self.assertTrue(self.registrant.has_unmet_device_need) - - # === Impairment line + assessment device request === - def test_impairment_and_device_request_on_assessment(self): - assessment = self.env["spp.disability.assessment"].create( - {"registrant_id": self.registrant.id, "assessment_date": date.today()} - ) - imp_type = self._code("urn:dci:cd:dr:01") - severity = self._code("urn:dci:cd:dr:02") - device_type = self._code("urn:dci:cd:dr:04") - if not (imp_type and severity and device_type): - self.skipTest("disability vocabulary codes not present") - - impairment = self.env["spp.disability.impairment"].create( - { - "assessment_id": assessment.id, - "impairment_type_id": imp_type.id, - "severity_level_id": severity.id, - } - ) - self.assertEqual(impairment.severity_sequence, severity.sequence) - - request = self.env["spp.disability.assessment.device.request"].create( - {"assessment_id": assessment.id, "device_type_id": device_type.id} - ) - self.assertEqual(request.status, "needed") diff --git a/spp_disability_registry/tests/test_registrant.py b/spp_disability_registry/tests/test_registrant.py index dbe1b3c0d..03d5d93b9 100644 --- a/spp_disability_registry/tests/test_registrant.py +++ b/spp_disability_registry/tests/test_registrant.py @@ -112,12 +112,6 @@ 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( [ @@ -141,9 +135,7 @@ def test_no_disability_with_draft_assessment(self): "registrant_id": self.registrant.id, "assessment_date": date.today(), "wg_seeing": "a_lot", - "impairment_line_ids": [ - (0, 0, {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_mild.id}) - ], + "severity_level_id": self.severity_mild.id if self.severity_mild else False, } ) # Recompute @@ -157,13 +149,7 @@ def test_disability_with_approved_assessment(self): "registrant_id": self.registrant.id, "assessment_date": date.today(), "wg_seeing": "a_lot", - "impairment_line_ids": [ - ( - 0, - 0, - {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_severe.id}, - ) - ], + "severity_level_id": self.severity_severe.id if self.severity_severe else False, "review_category": "mip", } ) @@ -186,9 +172,7 @@ def test_latest_approved_assessment_used(self): "registrant_id": self.registrant.id, "assessment_date": date.today() - relativedelta(months=6), "wg_seeing": "a_lot", - "impairment_line_ids": [ - (0, 0, {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_mild.id}) - ], + "severity_level_id": self.severity_mild.id if self.severity_mild else False, } ) old_assessment.write({"approval_state": "approved"}) @@ -199,13 +183,7 @@ def test_latest_approved_assessment_used(self): "registrant_id": self.registrant.id, "assessment_date": date.today(), "wg_seeing": "cannot", - "impairment_line_ids": [ - ( - 0, - 0, - {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_severe.id}, - ) - ], + "severity_level_id": self.severity_severe.id if self.severity_severe else False, } ) new_assessment.write({"approval_state": "approved"}) @@ -328,13 +306,7 @@ def test_household_member_disability_independent(self): "registrant_id": self.member1.id, "assessment_date": date.today(), "wg_walking": "cannot", - "impairment_line_ids": [ - ( - 0, - 0, - {"impairment_type_id": self.impairment_type.id, "severity_level_id": self.severity_severe.id}, - ) - ], + "severity_level_id": self.severity_severe.id if self.severity_severe else False, } ) assessment.write({"approval_state": "approved"}) diff --git a/spp_disability_registry/views/assessment_views.xml b/spp_disability_registry/views/assessment_views.xml index 1ab4715a4..105874e6b 100644 --- a/spp_disability_registry/views/assessment_views.xml +++ b/spp_disability_registry/views/assessment_views.xml @@ -5,31 +5,14 @@ spp.disability.assessment.form spp.disability.assessment -
    +
    - - - - - - - - - - - -
    - -