diff --git a/spp b/spp index f65088bd..fb6e086b 100755 --- a/spp +++ b/spp @@ -22,7 +22,14 @@ import shutil import subprocess import sys import time -import tomllib + +try: + import tomllib +except ModuleNotFoundError: + try: + import tomli as tomllib + except ModuleNotFoundError: + tomllib = None from pathlib import Path try: @@ -99,13 +106,15 @@ DEFAULT_CONFIG = { def load_config() -> dict: """Load config from ~/.spp.toml, returning defaults if not found.""" config = DEFAULT_CONFIG.copy() - if CONFIG_PATH.exists(): + if CONFIG_PATH.exists() and tomllib is not None: try: with open(CONFIG_PATH, "rb") as f: user_config = tomllib.load(f) config.update(user_config) except (tomllib.TOMLDecodeError, OSError) as e: warn(f"Could not load config from {CONFIG_PATH}: {e}") + elif CONFIG_PATH.exists() and tomllib is None: + warn("tomllib not available (Python < 3.11). Install 'tomli' or upgrade Python to load ~/.spp.toml config.") return config @@ -131,7 +140,7 @@ DEMO_PROFILES = { } -def run(cmd: str | list, check: bool = True, capture: bool = False, **kwargs) -> subprocess.CompletedProcess: +def run(cmd, check: bool = True, capture: bool = False, **kwargs) -> subprocess.CompletedProcess: """Run a command with nice defaults.""" if isinstance(cmd, str): kwargs.setdefault("shell", True) @@ -909,7 +918,7 @@ def cmd_sql(args): run(docker_compose("exec", "db", "psql", "-U", "odoo", "-d", db_name)) -def _show_url(open_browser: bool = False) -> str | None: +def _show_url(open_browser: bool = False): """Get the running Odoo server URL.""" service, profile = _find_running_odoo() if not service: diff --git a/spp_case_demo/README.rst b/spp_case_demo/README.rst index dd82c37e..e121b47b 100644 --- a/spp_case_demo/README.rst +++ b/spp_case_demo/README.rst @@ -7,7 +7,7 @@ OpenSPP Case Management Demo Data !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:39196739031b49c0d69e329806b940da071ca49472f130cd28ffd45eda6459e7 + !! source digest: sha256:force_regen !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -24,16 +24,20 @@ OpenSPP Case Management Demo Data Demo data generator for Case Management system. Creates realistic cases with intervention plans, home visits, progress notes, and service -referrals. Includes 9 fixed demo stories for training and sales demos, -plus configurable random case generation for volume testing. +referrals. Includes 9 fixed demo stories plus 3 background cases for +training and sales demos, and configurable volume case generation using +Faker for locale-aware random data (non-deterministic — each run +produces different results). Key Capabilities ~~~~~~~~~~~~~~~~ - Generate 9 fixed demo stories with predictable personas and case - progressions for consistent training scenarios -- Create random volume cases with configurable distribution percentages - for plans, visits, notes, and closures + progressions for consistent training scenarios, plus 3 background + cases (Fernandez Intake Pending, Johnson Assessment, Kim Case Closed) + for variety +- Create random volume cases using Faker (non-seeded) with configurable + distribution percentages for plans, visits, notes, and closures - Link generated cases to existing registrants or create standalone cases - Backdate case records and related activities to simulate realistic diff --git a/spp_case_demo/readme/DESCRIPTION.md b/spp_case_demo/readme/DESCRIPTION.md index 457abdb4..9199bee7 100644 --- a/spp_case_demo/readme/DESCRIPTION.md +++ b/spp_case_demo/readme/DESCRIPTION.md @@ -1,9 +1,9 @@ -Demo data generator for Case Management system. Creates realistic cases with intervention plans, home visits, progress notes, and service referrals. Includes 9 fixed demo stories for training and sales demos, plus configurable random case generation for volume testing. +Demo data generator for Case Management system. Creates realistic cases with intervention plans, home visits, progress notes, and service referrals. Includes 9 fixed demo stories plus 3 background cases for training and sales demos, and configurable volume case generation using Faker for locale-aware random data (non-deterministic — each run produces different results). ### Key Capabilities -- Generate 9 fixed demo stories with predictable personas and case progressions for consistent training scenarios -- Create random volume cases with configurable distribution percentages for plans, visits, notes, and closures +- Generate 9 fixed demo stories with predictable personas and case progressions for consistent training scenarios, plus 3 background cases (Fernandez Intake Pending, Johnson Assessment, Kim Case Closed) for variety +- Create random volume cases using Faker (non-seeded) with configurable distribution percentages for plans, visits, notes, and closures - Link generated cases to existing registrants or create standalone cases - Backdate case records and related activities to simulate realistic timelines over configurable day ranges - Create intervention plans with multiple interventions across case lifecycle stages diff --git a/spp_case_demo/static/description/index.html b/spp_case_demo/static/description/index.html index b8f84c13..343bfc4c 100644 --- a/spp_case_demo/static/description/index.html +++ b/spp_case_demo/static/description/index.html @@ -367,20 +367,24 @@

OpenSPP Case Management Demo Data

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:39196739031b49c0d69e329806b940da071ca49472f130cd28ffd45eda6459e7 +!! source digest: sha256:force_regen !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

Demo data generator for Case Management system. Creates realistic cases with intervention plans, home visits, progress notes, and service -referrals. Includes 9 fixed demo stories for training and sales demos, -plus configurable random case generation for volume testing.

+referrals. Includes 9 fixed demo stories plus 3 background cases for +training and sales demos, and configurable volume case generation using +Faker for locale-aware random data (non-deterministic — each run +produces different results).

Key Capabilities

+
+

Demo Programs

+

All programs use CEL expressions with activated registry variables:

+ +
+
+

Seeded Volume Generation

+

The SeededFarmGenerator uses random.Random(seed=42) for all +structural choices:

+ +

Running the generator twice with the same seed produces identical +output.

+
+
+

Security

+ ++++ + + + + + + + + + + +
GroupAccess
spp_security.group_spp_adminFull CRUD
+

Dependencies

spp_starter_farmer_registry, spp_demo, diff --git a/spp_grm_demo/README.rst b/spp_grm_demo/README.rst index fcccf247..cbf53f0c 100644 --- a/spp_grm_demo/README.rst +++ b/spp_grm_demo/README.rst @@ -7,7 +7,7 @@ OpenSPP GRM Demo Data !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + !! source digest: sha256:force_regen !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -24,9 +24,12 @@ OpenSPP GRM Demo Data Demo data generator for the Grievance Redress Mechanism. Creates both story-based tickets linked to specific personas (Juan Dela Cruz, Ibrahim -Hassan, Fatima Al-Rahman) and volume tickets using scenario templates. +Hassan, Fatima Al-Rahman, Ahmed Said, David Martinez, Maria Santos, Rosa +Garcia, Carlos Morales) and volume tickets using scenario templates. Simulates realistic ticket workflows including resolution paths, -escalations, and timeline distribution. +escalations, and timeline distribution. Uses Faker for locale-aware +random data (non-deterministic — each run produces different volume +tickets). Key Capabilities ~~~~~~~~~~~~~~~~ @@ -109,7 +112,8 @@ workflow demonstrations. Dependencies ~~~~~~~~~~~~ -``spp_demo``, ``spp_grm``, ``spp_security`` +``spp_demo``, ``spp_grm``, ``spp_grm_registry``, ``spp_grm_programs``, +``spp_security``, ``faker`` (Python) **Table of contents** diff --git a/spp_grm_demo/readme/DESCRIPTION.md b/spp_grm_demo/readme/DESCRIPTION.md index 5e108752..017c859f 100644 --- a/spp_grm_demo/readme/DESCRIPTION.md +++ b/spp_grm_demo/readme/DESCRIPTION.md @@ -1,4 +1,4 @@ -Demo data generator for the Grievance Redress Mechanism. Creates both story-based tickets linked to specific personas (Juan Dela Cruz, Ibrahim Hassan, Fatima Al-Rahman) and volume tickets using scenario templates. Simulates realistic ticket workflows including resolution paths, escalations, and timeline distribution. +Demo data generator for the Grievance Redress Mechanism. Creates both story-based tickets linked to specific personas (Juan Dela Cruz, Ibrahim Hassan, Fatima Al-Rahman, Ahmed Said, David Martinez, Maria Santos, Rosa Garcia, Carlos Morales) and volume tickets using scenario templates. Simulates realistic ticket workflows including resolution paths, escalations, and timeline distribution. Uses Faker for locale-aware random data (non-deterministic — each run produces different volume tickets). ### Key Capabilities @@ -50,4 +50,4 @@ Story personas align with `spp_mis_demo_v2` and `spp_case_demo` for cross-module ### Dependencies -`spp_demo`, `spp_grm`, `spp_security` +`spp_demo`, `spp_grm`, `spp_grm_registry`, `spp_grm_programs`, `spp_security`, `faker` (Python) diff --git a/spp_grm_demo/static/description/index.html b/spp_grm_demo/static/description/index.html index 390c5763..d35f158b 100644 --- a/spp_grm_demo/static/description/index.html +++ b/spp_grm_demo/static/description/index.html @@ -367,14 +367,17 @@

OpenSPP GRM Demo Data

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 +!! source digest: sha256:force_regen !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

Demo data generator for the Grievance Redress Mechanism. Creates both story-based tickets linked to specific personas (Juan Dela Cruz, Ibrahim -Hassan, Fatima Al-Rahman) and volume tickets using scenario templates. +Hassan, Fatima Al-Rahman, Ahmed Said, David Martinez, Maria Santos, Rosa +Garcia, Carlos Morales) and volume tickets using scenario templates. Simulates realistic ticket workflows including resolution paths, -escalations, and timeline distribution.

+escalations, and timeline distribution. Uses Faker for locale-aware +random data (non-deterministic — each run produces different volume +tickets).

Key Capabilities

Dependencies

-

spp_demo, spp_grm, spp_security

+

spp_demo, spp_grm, spp_grm_registry, spp_grm_programs, +spp_security, faker (Python)

Table of contents

    diff --git a/spp_mis_demo_v2/README.md b/spp_mis_demo_v2/README.md deleted file mode 100644 index 2076eaad..00000000 --- a/spp_mis_demo_v2/README.md +++ /dev/null @@ -1,346 +0,0 @@ -# OpenSPP MIS Demo V2 - -## Overview - -This module provides Demo Generator V2 for SP-MIS programs, following the simplified -"Fixed Stories + Volume" architecture. It creates predictable demo data that integrates -with the demo stories from `spp_demo` and showcases CEL expressions with Logic Packs -from `spp_studio`. - -## Features - -- **Demo Programs**: 6 programs with CEL eligibility expressions -- **Logic Pack Integration**: Programs link to reusable Logic Packs -- **CEL Expression Showcase**: Demonstrates member queries, metrics, and constants -- **Story Enrollments**: 9 demo personas with memorable names -- **Change Requests**: All 11 CR types with approval workflows -- **Multi-Mode Wizard**: Sales, Training, Testing, and Complete modes - -## Demo Programs - -The 6 demo programs showcase different CEL expression patterns using **activated -registry variables**: - -| Program | Target | CEL Pattern | Logic Pack | -| ------------------------ | ---------- | --------------------------------------------- | ------------------------ | -| Universal Child Grant | Group | `child_count > 0` (variable) | child_benefit | -| Elderly Social Pension | Individual | `age >= retirement_age` (computed + constant) | social_pension | -| Emergency Relief Fund | Group | `dependency_ratio >= 1.5` (computed variable) | vulnerability_assessment | -| Cash Transfer Program | Group | `hh_total_income < poverty_line` (aggregate) | cash_transfer_basic | -| Disability Support Grant | Group | `has_disabled_member` (computed variable) | disability_assistance | -| Food Assistance | Individual | `r.active == true` (simple field) | None (inline CEL) | - -## Demo Stories (12 Personas) - -### Primary Stories - Eligible - -| Persona | MIS Program | GRM Ticket | Case Story | -| ---------------- | ------------------------ | ------------------------- | ------------------------ | -| Maria Santos | Cash Transfer Program | Graduation inquiry | Santos Family Support | -| Juan Dela Cruz | Cash Transfer Program | Payment not received | Dela Cruz Emergency | -| Rosa Garcia | Elderly Pension + Food | Delivery schedule inquiry | Garcia Elder Care | -| Carlos Morales | Universal Child Grant | Adding new child | Morales Household Crisis | -| Ibrahim Hassan | Emergency Relief Fund | Resettlement support | Hassan Resettlement | -| David Martinez | Disability Support Grant | Grant application status | Martinez Disability | -| Fatima Al-Rahman | Universal Child Grant | Eligibility inquiry | Al-Rahman Assessment | -| Ahmed Said | Cash Transfer Program | Multiple tickets (3) | Said Family Support | - -### Rejection Demonstrations - Ineligible - -| Persona | Program Applied For | Rejection Reason | -| --------------------- | ---------------------- | ------------------------- | -| Mary Johnson | Elderly Social Pension | Below retirement age (55) | -| Childless Household | Universal Child Grant | No children under 18 | -| High Income Household | Cash Transfer Program | Income above poverty line | - -## CEL Expression Examples - -These expressions use **activated registry variables** for cleaner, more maintainable -eligibility rules. - -### Universal Child Grant - -```cel -r.is_group == true and child_count > 0 -``` - -Uses the `child_count` aggregate variable (automatically counts members under 18). - -### Elderly Social Pension - -```cel -r.is_group == false and age >= retirement_age -``` - -Uses the `age` computed variable and `retirement_age` constant (default: 60). - -### Emergency Relief Fund - -```cel -r.is_group == true and (dependency_ratio >= 1.5 or (is_female_headed and elderly_count > 0)) -``` - -Uses `dependency_ratio`, `is_female_headed`, and `elderly_count` variables for -vulnerability targeting. - -### Cash Transfer Program - -```cel -r.is_group == true and hh_total_income < poverty_line and hh_size >= 2 -``` - -Uses `hh_total_income` aggregate, `poverty_line` constant, and `hh_size` aggregate. - -### Disability Support Grant - -```cel -r.is_group == true and has_disabled_member -``` - -Uses the `has_disabled_member` computed variable (checks `is_person_with_disability` on -members). - -## Cross-Module Integration - -### Integrated Demo Ecosystem - -MIS Demo V2 is designed to work seamlessly with GRM and Case Management demos: - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ spp_mis_demo_v2│ │ spp_grm_demo │ │ spp_case_demo │ -│ │ │ │ │ │ -│ • Programs │────▶│ • Tickets │────▶│ • Cases │ -│ • Enrollments │ │ • Escalations │ │ • Interventions│ -│ • Payments │ │ • Resolutions │ │ • Plans │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - └───────────────────────┴───────────────────────┘ - Shared Personas (8 beneficiaries) -``` - -### Automatic Cross-Module Generation - -The MIS Demo wizard can automatically generate GRM tickets and Cases when those modules -are installed. Simply enable the options in the wizard: - -- **Generate GRM Demo**: Creates story-based tickets + volume tickets (requires - `spp_grm_demo`) -- **Generate Case Demo**: Creates story-based cases + volume cases (requires - `spp_case_demo`) - -This eliminates the need to run separate wizards for each module. - -### Manual Demo Order (Alternative) - -If you prefer to run demos separately: - -1. **spp_demo** - Creates base registrants with persona names -2. **spp_mis_demo_v2** - Creates programs and enrolls personas -3. **spp_grm_demo** - Creates tickets referencing program issues -4. **spp_case_demo** - Creates cases escalated from GRM - -### Demo Scenario: Juan Dela Cruz Journey - -1. **MIS**: Enrolled in Cash Transfer Program, receiving $150/month -2. **GRM**: Files ticket "Payment not received after house fire" -3. **GRM**: Ticket escalated due to emergency situation -4. **Case**: Emergency case opened with shelter and cash assistance interventions -5. **Case**: Family stabilized, case moves to monitoring phase - -### Demo Scenario: David Martinez Journey - -1. **MIS**: Applies for Disability Support Grant for son Miguel -2. **GRM**: Files ticket asking about application status -3. **GRM**: Referred to case management for comprehensive support -4. **Case**: Case opened for equipment assistance and education enrollment -5. **MIS**: Grant approved - $175/month (base $100 + $75 per disabled member) - -## Dependencies - -- `spp_demo` - Demo story infrastructure -- `spp_programs` - Program management -- `spp_registry` - Registry module -- `spp_cel_domain` - CEL variable system (ADR-008, ADR-017) -- `spp_studio` - Logic Packs and expressions -- `spp_change_request_v2` - Change request workflows -- Optional: `spp_grm_demo` - For cross-module GRM integration -- Optional: `spp_case_demo` - For cross-module case integration - -## Installation - -1. Install the module through Odoo Apps menu -2. Ensure `spp_demo` is installed with stories generated - -## Usage - -### Using the Wizard - -1. Navigate to **Programs > Generate MIS Demo Data** -2. Select **Demo Mode**: - - **Sales Demo**: Fixed stories, minimal data, fast - - **Partner Training**: Full programs, Logic Packs, comprehensive - - **Developer Testing**: Volume data, random generation, scale testing - - **Complete Demo**: All features enabled -3. Configure additional options as needed -4. Click **Generate Demo Data** - -### Demo Mode Presets - -| Mode | Stories | Programs | Logic Packs | Volume | Personas | -| -------- | ------- | -------- | ----------- | ------- | -------- | -| Sales | Yes | Yes | No | No | No | -| Training | Yes | Yes | Yes | Minimal | Yes | -| Testing | Yes | Yes | No | High | No | -| Complete | Yes | Yes | Yes | Yes | Yes | - -### Programmatic Usage - -```python -# Create and run the generator with training mode -generator = env['spp.mis.demo.wizard'].create({ - 'name': 'Training Demo', - 'demo_mode': 'training', - 'install_logic_packs': True, - 'include_test_personas': True, - 'create_demo_programs': True, - 'enroll_demo_stories': True, -}) -generator.action_generate_demo_data() -``` - -## Change Request Integration - -The demo creates change requests covering all 11 CR types: - -### CR Types Demonstrated - -| Type | Description | Demo State | -| ----------------- | ------------------------- | ------------------ | -| edit_individual | Basic data update | Approved | -| update_id | ID document update | Approved | -| exit_registrant | Registry exit | Approved + Applied | -| add_member | Add household member | Approved | -| remove_member | Remove member | Pending | -| transfer_member | Inter-household transfer | Pending | -| change_hoh | Head of household change | Approved | -| create_group | Create new group | Draft | -| split_household | Split into two households | Draft | -| merge_registrants | Merge duplicates | Rejected | - -### Workflow States Demonstrated - -- **Draft**: New CRs not yet submitted -- **Pending**: Awaiting approval -- **Approved**: Approved (may or may not be applied) -- **Rejected**: Rejected with reason documented -- **Revision**: Sent back for correction - -## Logic Pack Integration - -Programs link to pre-built Logic Packs from `spp_studio`: - -```python -DEMO_LOGIC_PACKS = [ - "child_benefit", # Universal Child Grant - "social_pension", # Elderly Social Pension - "vulnerability_assessment", # Emergency Relief - "cash_transfer_basic", # Cash Transfer Program - "disability_assistance", # Disability Support Grant -] -``` - -### Installing Logic Packs - -```python -from odoo.addons.spp_mis_demo_v2.models.demo_variables import install_demo_packs -installed = install_demo_packs(env) -``` - -## Registry Variables - -On module installation, registry variables are **automatically activated** and ready for -use in Logic Studio and program expressions. This includes both standard variables from -`spp_studio` and demo-specific variables. - -### Standard Variables (from spp_studio) - -| Category | Variables | -| ----------------------------- | --------------------------------------------------------------------------------------------------------- | -| **Demographics** | `age` | -| **Household Composition** | `hh_size`, `child_count`, `elderly_count`, `working_age_count` | -| **Household Characteristics** | `is_female_headed`, `is_elderly_headed`, `has_disabled_member`, `has_pregnant_member`, `dependency_ratio` | -| **Economic** | `per_capita_income`, `hh_total_income`, `hh_avg_income` | -| **Constants** | `poverty_line`, `retirement_age`, `child_age_limit`, `per_child_benefit`, `base_benefit` | - -### Demo-Specific Variables - -| Variable | Type | Default | Description | -| ----------------------------- | --------- | ------- | --------------------------- | -| `vulnerability_threshold` | constant | 70 | Emergency eligibility score | -| `base_child_grant` | constant | 50 | Per-child benefit amount | -| `disability_grant_base` | constant | 100 | Base disability amount | -| `disability_grant_per_member` | constant | 75 | Per disabled member bonus | -| `emergency_tier_1` | constant | 500 | Tier 1 emergency amount | -| `emergency_tier_2` | constant | 400 | Tier 2 emergency amount | -| `emergency_tier_3` | constant | 300 | Tier 3 emergency amount | -| `elderly_pension_amount` | constant | 100 | Fixed pension amount | -| `cash_transfer_amount` | constant | 150 | Fixed transfer amount | -| `disabled_count` | aggregate | - | Count of disabled members | - -### Variable Activation - -Variables are activated during module installation via `post_init_hook`. The activation: - -1. Finds variables by XML ID (e.g., `spp_studio.var_age`) -2. Activates any in `draft` state -3. Skips already active variables -4. Logs results for troubleshooting - -``` -[spp.mis.demo] Standard variables: 18 activated, 0 already active, 0 errors -[spp.mis.demo] Demo variables: 10 activated, 0 already active, 0 errors -[spp.mis.demo] Registry variables ready: 28 activated, 0 skipped, 0 errors -``` - -## Technical Details - -### Models - -- `spp.mis.demo.generator` - Core generator with all logic -- `spp.mis.demo.wizard` - Wizard interface (inherits generator) - -### Key Methods - -- `action_generate()` - Main entry point -- `_create_demo_programs()` - Creates 6 programs with CEL -- `_enroll_demo_stories()` - Enrolls personas -- `_install_logic_packs()` - Installs required Logic Packs -- `_create_test_personas()` - Creates test personas for Studio -- `_create_change_requests()` - Creates CR demos - -### Data Files - -- `data/demo_constants.xml` - CEL variable definitions -- `data/demo_personas.xml` - Test personas for Logic Studio -- `data/demo_programs.xml` - Program configurations - -## V3 Architecture Alignment - -This module follows the V3 Architecture principles: - -- **CEL Integration** - All eligibility uses CEL expressions -- **Logic Packs** - Reusable expression bundles -- **Fixed Stories + Volume** - Predictable personas plus random data -- **Multi-Mode** - Different presets for different use cases -- **Change Requests** - Full CR workflow coverage - -## License - -LGPL-3 - -## Credits - -**Authors**: OpenSPP.org - -**Maintainers**: jeremi, gonzalesedwin1123 diff --git a/spp_mis_demo_v2/README.rst b/spp_mis_demo_v2/README.rst index d57f29d6..912201b5 100644 --- a/spp_mis_demo_v2/README.rst +++ b/spp_mis_demo_v2/README.rst @@ -7,7 +7,7 @@ OpenSPP MIS Demo V2 !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:0c87dff4fb6bacbc90c381609a5bab672c40a2c8b950ad5e3a54eca0ac5ba0d5 + !! source digest: sha256:force_regen !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -22,31 +22,39 @@ OpenSPP MIS Demo V2 |badge1| |badge2| |badge3| -Demo data generator for SP-MIS programs. Creates 6 social protection +Demo data generator for SP-MIS programs. Creates 7 social protection programs with CEL eligibility expressions, enrolls 8 demo personas with -payment histories, and optionally generates volume data for testing. -Activates registry variables from ``spp_studio`` and installs Logic -Packs for eligibility rules. +payment histories, and optionally generates ~730 deterministic +households from seeded blueprints (``seed=42``) for reproducible volume +data. Activates registry variables from ``spp_studio`` and installs +Logic Packs for eligibility rules. Key Capabilities ~~~~~~~~~~~~~~~~ -- Generate 6 programs (Child Grant, Elderly Pension, Emergency Relief, - Cash Transfer, Disability Support, Food Assistance) with CEL - expressions +- Generate 7 programs (Universal Child Grant, Conditional Child Grant, + Elderly Pension, Emergency Relief, Cash Transfer, Disability Support, + Food Assistance) with CEL expressions - Enroll 8 demo personas with predefined stories and payment histories covering all demo scenarios +- Generate ~730 deterministic households with ~2555 members from 28 + blueprints via ``SeededVolumeGenerator`` with + ``random.Random(seed=42)`` — same seed always produces identical + output - Install Logic Packs from ``spp_studio`` for eligibility rules (child_benefit, social_pension, vulnerability_assessment, cash_transfer_basic, disability_assistance) - Activate registry variables (age, child_count, hh_total_income, dependency_ratio, etc.) via post_init_hook -- Generate volume data with configurable random enrollments for - dashboard testing +- 4 demo modes (Sales, Training, Testing, Complete) with automatic field + defaults +- Multi-locale support for name generation (fil_PH, si_LK, fr_TG) +- Geographic data loading for Philippines, Sri Lanka, and Togo - Create change requests at various workflow stages (draft, pending, approved, rejected) -- Cross-module integration: automatically creates GRM tickets and case - records when those modules are installed +- Cross-module integration: automatically creates GRM tickets, case + records, and Claim 169 QR credentials when those modules are installed +- Fairness analysis demo data and PRISM API client creation Key Models ~~~~~~~~~~ @@ -68,18 +76,12 @@ After installing: Testing, Complete) 3. Click "Load Demo Data" to generate -For automatic generation on install: - -.. code:: bash - - ODOO_INIT_MODULES=spp_mis_demo_v2 docker compose --profile ui up -d - For programmatic generation: .. code:: python - generator = env['spp.mis.demo.wizard'].create({'name': 'Demo'}) - generator.action_generate_demo_data() + wizard = env['spp.mis.demo.wizard'].create({}) + wizard.action_generate_demo_data() Demo Programs ~~~~~~~~~~~~~ @@ -88,6 +90,8 @@ All programs use CEL expressions with activated registry variables: - **Universal Child Grant**: ``r.is_group == true and child_count > 0`` (member aggregation) +- **Conditional Child Grant**: First 1,000 days targeting for young + children - **Elderly Social Pension**: ``r.is_group == false and age >= retirement_age`` (age computation) - **Emergency Relief Fund**: @@ -102,6 +106,23 @@ All programs use CEL expressions with activated registry variables: - **Food Assistance**: ``r.is_registrant == true and r.active == true`` (simple field comparison) +Seeded Volume Generation +~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``SeededVolumeGenerator`` uses ``random.Random(seed=42)`` for all +structural choices: + +- Ages, incomes, genders, and names from locale-specific pools +- Birthdates, registration dates, GPS coordinates +- Household structure from 28 deterministic blueprints across 5 + categories (young families, middle-age, elderly, working-age, + extended/vulnerable) +- Membership realism applied post-generation (83% enrolled, 10% exited, + 5% paused, 2% not eligible) + +Running the generator twice with the same seed produces identical +output. + UI Location ~~~~~~~~~~~ diff --git a/spp_mis_demo_v2/__manifest__.py b/spp_mis_demo_v2/__manifest__.py index 82e6148c..b166e37e 100644 --- a/spp_mis_demo_v2/__manifest__.py +++ b/spp_mis_demo_v2/__manifest__.py @@ -30,6 +30,8 @@ "spp_api_v2_gis", # QR Credentials (Claim 169) "spp_claim_169", + # Banking (for bank account demo data) + "spp_banking", # Demo-specific extensions ], "external_dependencies": {"python": ["requests"]}, diff --git a/spp_mis_demo_v2/data/demo_personas.xml b/spp_mis_demo_v2/data/demo_personas.xml index beeabe03..208fdaa0 100644 --- a/spp_mis_demo_v2/data/demo_personas.xml +++ b/spp_mis_demo_v2/data/demo_personas.xml @@ -27,13 +27,13 @@ Maria Santos SUCCESS STORY: 42-year-old rice farmer and mother of two from rural Laguna province. Started with 2.5 hectares and struggled with seasonal income. After 5 months in the Cash Transfer Program, she saved enough to buy better seeds and tools. Recently graduated from the program - demonstrating the pathway from poverty to self-sufficiency. Now mentors other farmers in her community. + >SUCCESS STORY: 42-year-old mother of two from rural Laguna province. Struggled with seasonal income and lived below the poverty line. After 5 months in the Cash Transfer Program, she saved enough to improve her household livelihood. Recently graduated from the program - demonstrating the pathway from poverty to self-sufficiency. Now mentors other beneficiaries in her community. eligible 10 {"name": "Maria Santos", "age": 42, "is_registrant": true, "is_group": false, "active": true, "farm_size_hectares": 2.5, "income": 8000, "occupation": "rice_farmer", "region": "rural"} + >{"name": "Maria Santos", "age": 42, "is_registrant": true, "is_group": false, "active": true, "income": 8000, "region": "rural"} @@ -64,18 +64,18 @@ >{"name": "Carlos Morales Household", "is_registrant": true, "is_group": true, "active": true, "hh_size": 5, "child_count": 3, "income": 4000, "head_occupation": "tricycle_driver", "spouse_occupation": "laundry_worker"} - + - Ibrahim Hassan + Ramon Gutierrez EMERGENCY RESPONSE: 50-year-old farmer displaced by recent flooding in his coastal village. Lost his home, crops, and livelihood. Currently staying in an evacuation center with vulnerability score of 85 (high). Receives Tier 2 emergency payments ($400) based on tiered vulnerability assessment - demonstrating crisis response targeting and ternary CEL expressions for benefit calculation. + >EMERGENCY RESPONSE: 50-year-old person displaced by recent flooding in his coastal village. Lost his home and livelihood. Currently staying in an evacuation center with vulnerability score of 85 (high). Receives Tier 2 emergency payments ($400) based on tiered vulnerability assessment - demonstrating crisis response targeting and ternary CEL expressions for benefit calculation. eligible 40 {"name": "Ibrahim Hassan", "age": 50, "is_registrant": true, "is_group": false, "active": true, "displacement_status": "displaced", "vulnerability_score": 85, "shelter_type": "evacuation_center", "previous_occupation": "farmer"} + >{"name": "Ramon Gutierrez", "age": 50, "is_registrant": true, "is_group": false, "active": true, "displacement_status": "displaced", "vulnerability_score": 85, "shelter_type": "evacuation_center"} @@ -106,9 +106,9 @@ >{"name": "Juan Dela Cruz", "age": 45, "is_registrant": true, "is_group": true, "active": true, "hh_size": 4, "income": 3500, "displacement_status": "displaced", "displacement_reason": "house_fire", "has_grm_ticket": true, "has_case": true} - + - Ahmed Said + Roberto Castillo MULTI-TICKET GRM: 38-year-old construction worker and father of 4 enrolled in Cash Transfer Program. Has filed multiple GRM tickets over time - payment delay (resolved), bank account update (resolved), and general inquiry about next payment. Demonstrates how the system tracks beneficiary interaction history and ticket patterns. @@ -117,12 +117,12 @@ 58 {"name": "Ahmed Said", "age": 38, "is_registrant": true, "is_group": false, "active": true, "income": 5000, "occupation": "construction_worker", "child_count": 4, "grm_ticket_count": 3} + >{"name": "Roberto Castillo", "age": 38, "is_registrant": true, "is_group": false, "active": true, "income": 5000, "occupation": "construction_worker", "child_count": 4, "grm_ticket_count": 3} - + - Fatima Al-Rahman + Teresa Villanueva INFORMATION REQUEST: 32-year-old mother of 2 who filed a GRM ticket asking about program eligibility requirements. Her ticket was quickly resolved with information provided. Demonstrates the system's ability to handle inquiries efficiently. She is now being assessed for Universal Child Grant enrollment. @@ -131,16 +131,16 @@ 60 {"name": "Fatima Al-Rahman", "age": 32, "is_registrant": true, "is_group": false, "active": true, "income": 4500, "child_count": 2, "marital_status": "married", "assessment_status": "pending"} + >{"name": "Teresa Villanueva", "age": 32, "is_registrant": true, "is_group": false, "active": true, "income": 4500, "child_count": 2, "marital_status": "married", "assessment_status": "pending"} - + - Mary Johnson + Lorna Pascual REJECTION CASE - AGE: 55-year-old woman who applied for Elderly Social Pension. Although she has no formal pension and low income, she was REJECTED because she doesn't meet the age requirement (retirement_age = 65). Her application demonstrates how the system properly enforces age-based eligibility rules. She should reapply in 10 years when she turns 65. @@ -149,7 +149,7 @@ 100 {"name": "Mary Johnson", "age": 55, "is_registrant": true, "is_group": false, "active": true, "income": 3000, "has_formal_pension": false, "rejection_reason": "below_retirement_age"} + >{"name": "Lorna Pascual", "age": 55, "is_registrant": true, "is_group": false, "active": true, "income": 3000, "has_formal_pension": false, "rejection_reason": "below_retirement_age"} diff --git a/spp_mis_demo_v2/data/event_types.xml b/spp_mis_demo_v2/data/event_types.xml index 9280ef5b..e0d7b422 100644 --- a/spp_mis_demo_v2/data/event_types.xml +++ b/spp_mis_demo_v2/data/event_types.xml @@ -39,7 +39,7 @@ training Training sessions for beneficiaries including agricultural practices, financial literacy, and program orientation. + >Training sessions for beneficiaries including financial literacy, life skills, and program orientation. visit both internal @@ -52,7 +52,7 @@ extension_visit Field extension visits by agricultural officers to provide technical assistance and monitor progress. + >Field extension visits by social welfare officers to provide technical assistance and monitor progress. visit both internal @@ -222,18 +222,18 @@ --> + Based on: completeness of data, household conditions, recommendations made --> 10 data_completeness,farm_condition_score,recommendations_count + >data_completeness,household_condition_score,recommendations_count visit_quality_score expression (int(data_completeness) * 0.4) + (int(farm_condition_score) * 0.4) + (int(recommendations_count) * 2.0) + >(int(data_completeness) * 0.4) + (int(household_condition_score) * 0.4) + (int(recommendations_count) * 2.0)

    Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

    -

    Demo data generator for SP-MIS programs. Creates 6 social protection +

    Demo data generator for SP-MIS programs. Creates 7 social protection programs with CEL eligibility expressions, enrolls 8 demo personas with -payment histories, and optionally generates volume data for testing. -Activates registry variables from spp_studio and installs Logic -Packs for eligibility rules.

    +payment histories, and optionally generates ~730 deterministic +households from seeded blueprints (seed=42) for reproducible volume +data. Activates registry variables from spp_studio and installs +Logic Packs for eligibility rules.

    Key Capabilities

      -
    • Generate 6 programs (Child Grant, Elderly Pension, Emergency Relief, -Cash Transfer, Disability Support, Food Assistance) with CEL -expressions
    • +
    • Generate 7 programs (Universal Child Grant, Conditional Child Grant, +Elderly Pension, Emergency Relief, Cash Transfer, Disability Support, +Food Assistance) with CEL expressions
    • Enroll 8 demo personas with predefined stories and payment histories covering all demo scenarios
    • +
    • Generate ~730 deterministic households with ~2555 members from 28 +blueprints via SeededVolumeGenerator with +random.Random(seed=42) — same seed always produces identical +output
    • Install Logic Packs from spp_studio for eligibility rules (child_benefit, social_pension, vulnerability_assessment, cash_transfer_basic, disability_assistance)
    • Activate registry variables (age, child_count, hh_total_income, dependency_ratio, etc.) via post_init_hook
    • -
    • Generate volume data with configurable random enrollments for -dashboard testing
    • +
    • 4 demo modes (Sales, Training, Testing, Complete) with automatic field +defaults
    • +
    • Multi-locale support for name generation (fil_PH, si_LK, fr_TG)
    • +
    • Geographic data loading for Philippines, Sri Lanka, and Togo
    • Create change requests at various workflow stages (draft, pending, approved, rejected)
    • -
    • Cross-module integration: automatically creates GRM tickets and case -records when those modules are installed
    • +
    • Cross-module integration: automatically creates GRM tickets, case +records, and Claim 169 QR credentials when those modules are installed
    • +
    • Fairness analysis demo data and PRISM API client creation
    @@ -427,14 +435,10 @@

    Configuration

    Testing, Complete)
  • Click “Load Demo Data” to generate
  • -

    For automatic generation on install:

    -
    -ODOO_INIT_MODULES=spp_mis_demo_v2 docker compose --profile ui up -d
    -

    For programmatic generation:

    -generator = env['spp.mis.demo.wizard'].create({'name': 'Demo'})
    -generator.action_generate_demo_data()
    +wizard = env['spp.mis.demo.wizard'].create({})
    +wizard.action_generate_demo_data()
     
    @@ -443,6 +447,8 @@

    Demo Programs

    • Universal Child Grant: r.is_group == true and child_count > 0 (member aggregation)
    • +
    • Conditional Child Grant: First 1,000 days targeting for young +children
    • Elderly Social Pension: r.is_group == false and age >= retirement_age (age computation)
    • Emergency Relief Fund: @@ -458,6 +464,22 @@

      Demo Programs

      (simple field comparison)
    +
    +

    Seeded Volume Generation

    +

    The SeededVolumeGenerator uses random.Random(seed=42) for all +structural choices:

    +
      +
    • Ages, incomes, genders, and names from locale-specific pools
    • +
    • Birthdates, registration dates, GPS coordinates
    • +
    • Household structure from 28 deterministic blueprints across 5 +categories (young families, middle-age, elderly, working-age, +extended/vulnerable)
    • +
    • Membership realism applied post-generation (83% enrolled, 10% exited, +5% paused, 2% not eligible)
    • +
    +

    Running the generator twice with the same seed produces identical +output.

    +

    UI Location

      diff --git a/spp_mis_demo_v2/tests/test_blueprint_reproducibility.py b/spp_mis_demo_v2/tests/test_blueprint_reproducibility.py index fdc3082e..0a92eca2 100644 --- a/spp_mis_demo_v2/tests/test_blueprint_reproducibility.py +++ b/spp_mis_demo_v2/tests/test_blueprint_reproducibility.py @@ -125,6 +125,7 @@ def test_eligibility_flags_exist_for_known_programs(self): """Each blueprint's eligibility flags reference valid program IDs.""" valid_program_ids = { "universal_child_grant", + "conditional_child_grant", "elderly_social_pension", "emergency_relief_fund", "cash_transfer_program", diff --git a/spp_mis_demo_v2/tests/test_claim169_demo.py b/spp_mis_demo_v2/tests/test_claim169_demo.py index 6259ba53..a6b57ea9 100644 --- a/spp_mis_demo_v2/tests/test_claim169_demo.py +++ b/spp_mis_demo_v2/tests/test_claim169_demo.py @@ -30,12 +30,12 @@ def setUpClass(cls): # Create test country for locale cls.test_country = cls.env.ref("base.us") - # Create a demo story registrant + # Create a demo story registrant (group named by family name) cls.demo_registrant = cls.env["res.partner"].create( { - "name": "Maria Santos", + "name": "Santos", "is_registrant": True, - "is_group": False, + "is_group": True, } ) diff --git a/spp_mis_demo_v2/tests/test_demo_programs.py b/spp_mis_demo_v2/tests/test_demo_programs.py index 6880fe0c..eb1ac8b1 100644 --- a/spp_mis_demo_v2/tests/test_demo_programs.py +++ b/spp_mis_demo_v2/tests/test_demo_programs.py @@ -428,28 +428,34 @@ def test_story_program_alignment_complete(self): Each story persona should be enrolled in programs that match their: - Demographics (age, household composition) - Circumstances (income, disability status, vulnerability) - - Story narrative (farmer, elderly, displaced, etc.) + - Story narrative (elderly, displaced, disability, etc.) """ from odoo.addons.spp_mis_demo_v2.models import demo_programs # Expected story-to-program mappings based on V3 spec expected_mappings = { - # Maria Santos - Farmer success story with graduation from Cash Transfer - "maria_santos": ["Cash Transfer Program"], + # Maria Santos - Cash Transfer (graduated) + Universal Child Grant + Food Assistance (individual) + "maria_santos": ["Cash Transfer Program", "Universal Child Grant", "Food Assistance"], # Juan Dela Cruz - Cash Transfer recipient with GRM issue "juan_dela_cruz": ["Cash Transfer Program"], # Rosa Garcia - Elderly + Food Assistance "rosa_garcia": ["Elderly Social Pension", "Food Assistance"], # Carlos/Elena Morales - Household with 3 children "carlos_elena_morales": ["Universal Child Grant"], - # Ibrahim Hassan - Displaced/Vulnerable - "ibrahim_hassan": ["Emergency Relief Fund"], - # Fatima Al-Rahman - Food Assistance recipient + # Ramon Gutierrez - Emergency Relief (household) + Food Assistance (individual) + "ibrahim_hassan": ["Emergency Relief Fund", "Food Assistance"], + # Teresa Villanueva - Food Assistance recipient "fatima_al_rahman": ["Food Assistance"], # David/Sofia Martinez - Household with disabled member - "david_martinez": ["Disability Support Grant"], - # Ahmed Said - Background Cash Transfer + "david_sofia_martinez": ["Disability Support Grant"], + # Roberto Castillo - Background Cash Transfer "ahmed_said": ["Cash Transfer Program"], + # Manuel Pangilinan - Elderly Social Pension (individual) + "manuel_gloria_elderly": ["Elderly Social Pension"], + # Pedro Reyes - Food Assistance + "pedro_reyes": ["Food Assistance"], + # Ana Mendoza - Food Assistance + "ana_mendoza": ["Food Assistance"], } for story_id, expected_programs in expected_mappings.items(): @@ -544,14 +550,14 @@ def test_disability_story_eligibility(self): """ from odoo.addons.spp_mis_demo_v2.models import demo_programs - programs = demo_programs.get_programs_for_story("david_martinez") + programs = demo_programs.get_programs_for_story("david_sofia_martinez") program_names = [p["name"] for p in programs] self.assertIn("Disability Support Grant", program_names) def test_emergency_story_eligibility(self): - """Test Ibrahim Hassan meets emergency eligibility criteria. + """Test Ramon Gutierrez meets emergency eligibility criteria. - Ibrahim should be eligible because: + Ramon Gutierrez should be eligible because: - dependency_ratio >= 1.5 (using computed variable) - OR is_female_headed and elderly_count > 0 """ @@ -561,10 +567,100 @@ def test_emergency_story_eligibility(self): program_names = [p["name"] for p in programs] self.assertIn("Emergency Relief Fund", program_names) + # =================================================================== + # Compliance criteria tests + # =================================================================== + + def test_cash_transfer_has_compliance_expression(self): + """Cash Transfer Program must define a compliance CEL expression.""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + for prog in demo_programs.get_all_demo_programs(): + if prog["id"] == "cash_transfer_program": + self.assertIn("compliance_cel_expression", prog) + self.assertEqual( + prog["compliance_cel_expression"], + "per_capita_income < poverty_line", + ) + return + self.fail("Cash Transfer Program not found") + + def test_conditional_child_grant_has_compliance_expression(self): + """Conditional Child Grant must define a compliance CEL expression.""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + for prog in demo_programs.get_all_demo_programs(): + if prog["id"] == "conditional_child_grant": + self.assertIn("compliance_cel_expression", prog) + self.assertEqual( + prog["compliance_cel_expression"], + "per_capita_income < income_threshold", + ) + return + self.fail("Conditional Child Grant not found") + + def test_conditional_child_grant_has_program_constants(self): + """Conditional Child Grant must override income_threshold to 2000.""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + for prog in demo_programs.get_all_demo_programs(): + if prog["id"] == "conditional_child_grant": + self.assertIn("program_constants", prog) + self.assertEqual(prog["program_constants"]["income_threshold"], "2000") + return + self.fail("Conditional Child Grant not found") + + def test_programs_without_compliance_have_no_expression(self): + """Programs without compliance should not define compliance_cel_expression.""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + programs_with_compliance = {"cash_transfer_program", "conditional_child_grant"} + for prog in demo_programs.get_all_demo_programs(): + if prog["id"] not in programs_with_compliance: + expr = prog.get("compliance_cel_expression") + self.assertFalse( + expr, + f"Program '{prog['name']}' should not have compliance_cel_expression", + ) + + def test_santos_has_non_compliant_cycle(self): + """Santos enrollment must define non_compliant_cycle for compliance failure.""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + enrollments = demo_programs.get_story_enrollments("maria_santos") + cash_transfer = [e for e in enrollments if e["program"] == "Cash Transfer Program"] + self.assertEqual(len(cash_transfer), 1) + self.assertIn("non_compliant_cycle", cash_transfer[0]) + self.assertIn("days_back", cash_transfer[0]["non_compliant_cycle"]) + + def test_santos_has_graduated_days_back(self): + """Santos Cash Transfer must have graduated_days_back (exited after compliance failure).""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + enrollments = demo_programs.get_story_enrollments("maria_santos") + cash_transfer = [e for e in enrollments if e["program"] == "Cash Transfer Program"] + self.assertEqual(len(cash_transfer), 1) + self.assertIn("graduated_days_back", cash_transfer[0]) + + def test_santos_enrolled_in_universal_child_grant(self): + """Santos must also be enrolled in Universal Child Grant (partial exit story).""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + enrollments = demo_programs.get_story_enrollments("maria_santos") + programs = [e["program"] for e in enrollments] + self.assertIn("Universal Child Grant", programs) + + def test_only_two_programs_have_compliance(self): + """Exactly 2 programs should have compliance CEL expressions.""" + from odoo.addons.spp_mis_demo_v2.models import demo_programs + + with_compliance = [p for p in demo_programs.get_all_demo_programs() if p.get("compliance_cel_expression")] + self.assertEqual(len(with_compliance), 2) + def test_rejected_story_documented(self): """Test that rejected application story is properly documented. - Mary Johnson should be rejected for age requirement not met. + Lorna Pascual should be rejected for age requirement not met. """ from odoo.addons.spp_mis_demo_v2.models import demo_programs diff --git a/spp_mis_demo_v2/tests/test_mis_demo_generator.py b/spp_mis_demo_v2/tests/test_mis_demo_generator.py index e51123d0..38d7dfe2 100644 --- a/spp_mis_demo_v2/tests/test_mis_demo_generator.py +++ b/spp_mis_demo_v2/tests/test_mis_demo_generator.py @@ -460,13 +460,17 @@ def setUpClass(cls): # Based on demo_stories.py definitions cls.EXPECTED_HOUSEHOLD_MEMBERS = { # story_name: expected_member_count - "Carlos Morales": 5, # head + spouse + 3 children - "Amina Osman": 4, # head + 3 children - "Jose Reyes Sr": 8, # head + spouse + 2 adults + 4 children - "Chen Wei": 7, # head + spouse + 5 children - "Manuel Santos": 2, # head + spouse - "James Nguyen": 4, # head + 3 adults - "David Martinez": 3, # head + spouse + 1 child + "Morales": 5, # head + spouse + 3 children + "Santos": 5, # head + spouse + 2 children + 1 adult + "Dela Cruz": 4, # head + spouse + 2 children + "Gutierrez": 7, # head + spouse + 5 children + "Castillo": 3, # head + spouse + 1 child + "Aquino": 4, # head + 3 children + "Reyes": 8, # head + spouse + 2 adults + 4 children + "Bautista": 7, # head + spouse + 5 children + "Pangilinan": 2, # head + spouse + "Navarro": 4, # head + 3 adults + "Martinez": 3, # head + spouse + 1 child } def _run_generator_for_stories(self): @@ -512,72 +516,71 @@ def test_household_stories_created_with_members(self): f"Household '{story_name}' should have {expected_count} members, but has {member_count}", ) - def test_chen_family_has_all_members(self): - """Test Chen family has head, spouse, and all 5 children.""" + def test_bautista_family_has_all_members(self): + """Test Bautista family has head, spouse, and all 5 children.""" self._run_generator_for_stories() group = self.env["res.partner"].search( [ - ("name", "=", "Chen Wei"), + ("name", "=", "Bautista"), ("is_group", "=", True), ], limit=1, ) - self.assertTrue(group, "Chen Wei household not found") + self.assertTrue(group, "Eduardo Bautista household not found") memberships = self.env["spp.group.membership"].search([("group", "=", group.id)]) member_names = memberships.mapped("individual.name") # Expected members - names stored as "FAMILY, GIVEN" uppercase - # e.g., "Chen Wei" becomes "WEI, CHEN" - expected_given_names = ["Wei", "Mei", "Ling", "Jun", "Xiao", "Yan", "Bo"] + expected_given_names = ["Eduardo", "Carmen", "Patricia", "Fernando", "Lucia", "Rosalie", "Antonio"] - self.assertEqual(len(memberships), 7, "Chen family should have 7 members") + self.assertEqual(len(memberships), 7, "Bautista family should have 7 members") # Check each expected member exists by given name for given_name in expected_given_names: found = any(given_name.upper() in name.upper() for name in member_names) self.assertTrue( found, - f"Expected member with given name '{given_name}' not found in Chen family. " + f"Expected member with given name '{given_name}' not found in Bautista family. " f"Actual members: {member_names}", ) - def test_nguyen_family_has_all_adults(self): - """Test Nguyen extended family has head and all 3 adults.""" + def test_navarro_family_has_all_adults(self): + """Test Navarro extended family has head and all 3 adults.""" self._run_generator_for_stories() group = self.env["res.partner"].search( [ - ("name", "=", "James Nguyen"), + ("name", "=", "Navarro"), ("is_group", "=", True), ], limit=1, ) - self.assertTrue(group, "James Nguyen household not found") + self.assertTrue(group, "Ricardo Navarro household not found") memberships = self.env["spp.group.membership"].search([("group", "=", group.id)]) member_names = memberships.mapped("individual.name") # Expected members - names stored as "FAMILY, GIVEN" uppercase - expected_given_names = ["James", "Linda", "Michael", "Sarah"] + expected_given_names = ["Ricardo", "Lourdes", "Eduardo", "Cristina"] - self.assertEqual(len(memberships), 4, "Nguyen family should have 4 members") + self.assertEqual(len(memberships), 4, "Navarro family should have 4 members") for given_name in expected_given_names: found = any(given_name.upper() in name.upper() for name in member_names) self.assertTrue( found, - f"Expected member with given name '{given_name}' not found in Nguyen family. " + f"Expected member with given name '{given_name}' not found in Navarro family. " f"Actual members: {member_names}", ) def test_existing_empty_group_gets_members_populated(self): """Test that existing empty household groups get their members created.""" - # First, delete any existing Chen Wei group to simulate a fresh state + # First, delete any existing Bautista group to simulate a fresh state existing = self.env["res.partner"].search( [ - ("name", "=", "Chen Wei"), + ("name", "=", "Bautista"), ("is_registrant", "=", True), ("is_group", "=", True), ] @@ -587,10 +590,10 @@ def test_existing_empty_group_gets_members_populated(self): self.env["spp.group.membership"].search([("group", "in", existing.ids)]).unlink() existing.unlink() - # Create an empty Chen Wei group (simulating partial creation bug) + # Create an empty Bautista group (simulating partial creation bug) empty_group = self.env["res.partner"].create( { - "name": "Chen Wei", + "name": "Bautista", "is_registrant": True, "is_group": True, } @@ -605,7 +608,7 @@ def test_existing_empty_group_gets_members_populated(self): # Now check members were created final_count = self.env["spp.group.membership"].search_count([("group", "=", empty_group.id)]) - self.assertEqual(final_count, 7, "Chen Wei group should have 7 members after generator runs") + self.assertEqual(final_count, 7, "Bautista group should have 7 members after generator runs") def test_head_of_household_has_correct_membership_type(self): """Test that household heads have the 'head' membership type.""" @@ -618,7 +621,7 @@ def test_head_of_household_has_correct_membership_type(self): self.skipTest("Head membership type not configured") # Check a few households - for story_name in ["Chen Wei", "James Nguyen", "Carlos Morales"]: + for story_name in ["Bautista", "Navarro", "Morales"]: group = self.env["res.partner"].search( [ ("name", "=", story_name), @@ -647,10 +650,10 @@ def test_idempotent_member_creation(self): # Run generator first time self._run_generator_for_stories() - # Count members for Chen family + # Count members for Bautista family group = self.env["res.partner"].search( [ - ("name", "=", "Chen Wei"), + ("name", "=", "Bautista"), ("is_group", "=", True), ], limit=1, @@ -669,36 +672,36 @@ def test_individual_members_have_correct_attributes(self): """Test that created individual members have correct attributes.""" self._run_generator_for_stories() - # Find Chen Mei (spouse of Chen Wei) - chen_mei = self.env["res.partner"].search( + # Find Carmen Bautista (spouse of Eduardo Bautista) + carmen = self.env["res.partner"].search( [ - ("name", "ilike", "%chen%mei%"), + ("name", "ilike", "%carmen%bautista%"), ("is_registrant", "=", True), ("is_group", "=", False), ], limit=1, ) - if not chen_mei: + if not carmen: # Try alternative name format - chen_mei = self.env["res.partner"].search( + carmen = self.env["res.partner"].search( [ - ("name", "ilike", "%mei%chen%"), + ("name", "ilike", "%bautista%carmen%"), ("is_registrant", "=", True), ("is_group", "=", False), ], limit=1, ) - self.assertTrue(chen_mei, "Chen Mei individual should be created") - self.assertFalse(chen_mei.is_group, "Chen Mei should not be a group") - self.assertTrue(chen_mei.is_registrant, "Chen Mei should be a registrant") + self.assertTrue(carmen, "Carmen Bautista individual should be created") + self.assertFalse(carmen.is_group, "Carmen Bautista should not be a group") + self.assertTrue(carmen.is_registrant, "Carmen Bautista should be a registrant") # Check birthdate is set (age 44 in story) - if chen_mei.birthdate: + if carmen.birthdate: from datetime import date - age = (date.today() - chen_mei.birthdate).days // 365 + age = (date.today() - carmen.birthdate).days // 365 self.assertGreaterEqual(age, 40) self.assertLessEqual(age, 50) @@ -706,8 +709,8 @@ def test_get_story_name_returns_correct_names_for_all_stories(self): """Test that _get_story_name returns correct names for all demo stories. This test ensures that story IDs are properly mapped to registrant names, - preventing duplicate registrants with incorrect names like "Chen Large Family" - instead of "Chen Wei". + preventing duplicate registrants with incorrect names derived from story IDs + instead of the actual story name. """ from odoo.addons.spp_demo.models import demo_stories @@ -733,3 +736,137 @@ def test_get_story_name_returns_correct_names_for_all_stories(self): f"but got '{actual_name}'. This can cause duplicate registrants " f"with incorrect names.", ) + + # =================================================================== + # Compliance manager tests + # =================================================================== + + def test_compliance_managers_configured_after_generation(self): + """Test that compliance managers have CEL expressions after program creation.""" + generator = self.env["spp.mis.demo.generator"].create( + { + "name": "Test Compliance", + "create_demo_programs": True, + "enroll_demo_stories": False, + "generate_volume": False, + "create_cycles": False, + "locale_origin": self.test_country.id, + } + ) + generator.action_generate() + + # Cash Transfer should have compliance CEL + cash_transfer = self.env["spp.program"].search([("name", "=", "Cash Transfer Program")], limit=1) + self.assertTrue(cash_transfer, "Cash Transfer Program not found") + self.assertTrue(cash_transfer.compliance_manager_ids, "No compliance manager on Cash Transfer") + + for wrapper in cash_transfer.compliance_manager_ids: + concrete = wrapper.manager_ref_id + if hasattr(concrete, "compliance_cel_expression"): + self.assertEqual( + concrete.compliance_cel_expression, + "per_capita_income < poverty_line", + ) + break + else: + self.fail("No concrete compliance manager found for Cash Transfer") + + def test_conditional_child_grant_compliance_configured(self): + """Test that Conditional Child Grant has compliance CEL and program constant.""" + generator = self.env["spp.mis.demo.generator"].create( + { + "name": "Test CCG Compliance", + "create_demo_programs": True, + "enroll_demo_stories": False, + "generate_volume": False, + "create_cycles": False, + "locale_origin": self.test_country.id, + } + ) + generator.action_generate() + + ccg = self.env["spp.program"].search([("name", "=", "Conditional Child Grant")], limit=1) + self.assertTrue(ccg, "Conditional Child Grant not found") + + # Check compliance expression + for wrapper in ccg.compliance_manager_ids: + concrete = wrapper.manager_ref_id + if hasattr(concrete, "compliance_cel_expression"): + self.assertEqual( + concrete.compliance_cel_expression, + "per_capita_income < income_threshold", + ) + break + else: + self.fail("No concrete compliance manager found for CCG") + + # Check program constant override + param = self.env["spp.cel.program.parameter"].search([("program_id", "=", ccg.id)], limit=1) + self.assertTrue(param, "No program parameter found for CCG") + self.assertEqual(param.value, "2000") + + def test_non_compliant_cycle_membership_created(self): + """Test that Santos has non_compliant cycle membership after generation.""" + generator = self.env["spp.mis.demo.generator"].create( + { + "name": "Test Non-Compliant", + "create_demo_programs": True, + "enroll_demo_stories": True, + "create_story_payments": True, + "generate_volume": False, + "create_cycles": False, + "locale_origin": self.test_country.id, + } + ) + generator.action_generate() + + # Find Santos household + santos = self.env["res.partner"].search( + [("name", "=", "Santos"), ("is_group", "=", True), ("is_registrant", "=", True)], + limit=1, + ) + if not santos: + # Story registrant may not have been created in test environment + return + + # Check Cash Transfer cycle membership is non_compliant + cash_transfer = self.env["spp.program"].search([("name", "=", "Cash Transfer Program")], limit=1) + if not cash_transfer: + return + + cycle = self.env["spp.cycle"].search([("program_id", "=", cash_transfer.id)], limit=1) + if not cycle: + return + + cm = self.env["spp.cycle.membership"].search( + [("partner_id", "=", santos.id), ("cycle_id", "=", cycle.id)], + limit=1, + ) + self.assertTrue(cm, "No cycle membership found for Santos in Cash Transfer") + self.assertEqual(cm.state, "non_compliant", "Santos should be non_compliant") + + def test_programs_without_compliance_have_empty_expression(self): + """Programs without compliance_cel_expression should have empty compliance managers.""" + generator = self.env["spp.mis.demo.generator"].create( + { + "name": "Test No Compliance", + "create_demo_programs": True, + "enroll_demo_stories": False, + "generate_volume": False, + "create_cycles": False, + "locale_origin": self.test_country.id, + } + ) + generator.action_generate() + + # Food Assistance should NOT have a compliance expression + food = self.env["spp.program"].search([("name", "=", "Food Assistance")], limit=1) + self.assertTrue(food, "Food Assistance not found") + + for wrapper in food.compliance_manager_ids: + concrete = wrapper.manager_ref_id + if hasattr(concrete, "compliance_cel_expression"): + self.assertFalse( + concrete.compliance_cel_expression, + "Food Assistance should not have a compliance expression", + ) diff --git a/spp_mis_demo_v2/views/mis_demo_wizard_view.xml b/spp_mis_demo_v2/views/mis_demo_wizard_view.xml index 696da44b..ad31927a 100644 --- a/spp_mis_demo_v2/views/mis_demo_wizard_view.xml +++ b/spp_mis_demo_v2/views/mis_demo_wizard_view.xml @@ -31,7 +31,7 @@

      • 6 social protection programs (Child Grant, Pension, Emergency Relief, Cash Transfer, Disability Support, Food Assistance)
      • + >7 social protection programs (Child Grant, Conditional Child Grant, Pension, Emergency Relief, Cash Transfer, Disability Support, Food Assistance)
      • 8 demo personas with full stories and payment history
      • 50: + expr = expr[:47] + "..." + summary_parts.append(f"CEL: {expr}") else: # Fallback to other summary details if hasattr(manager, "admin_area_ids") and manager.admin_area_ids: