From f3f37786c04edd456f3c6f736c58774e5c7adc66 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 5 May 2026 12:44:02 +0800 Subject: [PATCH 01/23] docs(spp_farmer_registry_demo): rewrite USE_CASES.md + close eligibility/compliance/approval gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites `spp_farmer_registry_demo/docs/USE_CASES.md` to mirror the canonical structure of `spp_mis_demo_v2/docs/USE_CASES.md` (8 farm stories with FM1-FM8 codes, 3 edge cases, 2 cooperatives, 9 demo scenarios, References section with constellation tables, program configuration, change requests, geographic distribution, overview metrics). Filipino-only locale tables — the farmer generator does not yet implement Togolese / Sri Lankan name pools. Along the way, fix or fill a handful of demo gaps surfaced while walking the doc against the running demo: - **Eligibility consistency.** Mirror the MIS fix from commit `aa6da89d`: a new `_configure_eligibility_manager` writes the program's `cel_expression` onto the eligibility manager's concrete record after wizard creation. Without this the manager has no CEL set, so seeded enrollments don't match what `Verify Eligibility` re-evaluates and registrants get demoted to Not Eligible. - **Compliance for Input Subsidy + Equipment Grant.** Add a `compliance_cel_expression` to both program defs and a sibling `_configure_compliance_manager` that wires it through. Mirrors how MIS handles Cash Transfer / Conditional Child Grant compliance. - **Approval definitions.** New `data/approval_definitions.xml` with Cycle / Entitlement / Entitlement-cycle-approver / In-kind defs, plus `_configure_program_approvals` that sets each program's cycle manager and entitlement manager `approval_definition_id` so cycles and entitlements actually enter the approval workflow. MIS demo ships the XML but never assigns the defs to the managers — that gap is for a separate ticket. - **CEL activity-count variables.** `crop_count`, `livestock_count`, `aquaculture_count`, `land_parcel_count`, `total_livestock_heads` in `spp_farmer_registry/data/cel_variables.xml` were declared with `source_type="aggregate"` against non-existent inverse names like `farm_aquaculture_activities` (the real o2m is `farm_aquaculture_act_ids`), and the framework's `aggregate_target` is a Selection limited to members/enrollments/entitlements anyway, so the lookup never resolved. Net effect: every CEL using these variables read 0 for every farm, so demo programs' CELs returned 0 matches even with seeded enrollments. Switched all five to `source_type="field"` reading from the existing stored Integer computes on `spp.farm`, and dropped `cache_strategy="ttl"` (which was routing them through `spp.data.value` cache and reading 0) in favour of `cache_strategy="none"` so the resolver inlines them as direct field reads. Flipped the file's `noupdate="1"` to `"0"` so the corrected definitions land on existing installs at upgrade. --- spp_farmer_registry/__manifest__.py | 2 +- spp_farmer_registry/data/cel_variables.xml | 78 +- spp_farmer_registry_demo/__manifest__.py | 3 +- .../data/approval_definitions.xml | 83 ++ spp_farmer_registry_demo/docs/USE_CASES.md | 1152 +++++++---------- .../models/demo_programs.py | 13 + .../models/farmer_demo_generator.py | 141 ++ 7 files changed, 769 insertions(+), 703 deletions(-) create mode 100644 spp_farmer_registry_demo/data/approval_definitions.xml diff --git a/spp_farmer_registry/__manifest__.py b/spp_farmer_registry/__manifest__.py index fc0b9ef73..553eb16c5 100644 --- a/spp_farmer_registry/__manifest__.py +++ b/spp_farmer_registry/__manifest__.py @@ -3,7 +3,7 @@ "name": "OpenSPP Farmer Registry", "summary": "Farmer Registry with vocabulary-based fields, CEL variables, and Logic Studio integration", "category": "OpenSPP", - "version": "19.0.2.0.0", + "version": "19.0.2.0.2", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_farmer_registry/data/cel_variables.xml b/spp_farmer_registry/data/cel_variables.xml index bf6a60f5a..bfa7e7b88 100644 --- a/spp_farmer_registry/data/cel_variables.xml +++ b/spp_farmer_registry/data/cel_variables.xml @@ -7,7 +7,11 @@ - Benefit calculations (farm_size_hectares * per_hectare_rate) - Program targeting (crop_count, livestock_count) --> - + + @@ -78,6 +82,23 @@ AGGREGATE VARIABLES - Activity Counts ═══════════════════════════════════════════════════════════════════════ --> + + crop_count Number of Crop Activities @@ -86,15 +107,13 @@ >Count of crop cultivation activities on the farm number - aggregate - count - farm_crop_activities - true + field + res.partner + crop_activity_count crop_count group True - ttl - 86400 + none @@ -105,15 +124,13 @@ >Count of livestock rearing activities on the farm number - aggregate - count - farm_livestock_activities - true + field + res.partner + livestock_activity_count livestock_count group True - ttl - 86400 + none @@ -122,15 +139,13 @@ Count of aquaculture activities on the farm number - aggregate - count - farm_aquaculture_activities - true + field + res.partner + aquaculture_activity_count aquaculture_count group True - ttl - 86400 + none @@ -139,34 +154,31 @@ Count of registered land parcels for the farm number - aggregate - count - land_parcels - true + field + res.partner + land_parcel_count land_parcel_count group True - ttl - 86400 + none - + total_livestock_heads Total Livestock Heads Sum of all livestock across all activities number - aggregate - sum - farm_livestock_activities - quantity - true + field + res.partner + total_livestock_heads total_livestock_heads group True - ttl - 86400 + none + + + + Farmer: Cycle Approval - Program Manager + + group + + False + True + 3 + True + + + + + Farmer: Entitlement Approval - Program Manager + + group + + False + True + 3 + True + + + + + Farmer: Entitlement Approval - Cycle Approver + + group + + False + True + 3 + True + + + + + Farmer: In-Kind Entitlement Approval - Program Manager + + group + + False + True + 3 + True + + diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index a3815f704..267e50a85 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -1,902 +1,718 @@ -# OpenSPP Farmer Registry Demo - Use Cases Guide +# Farmer Registry Demo — Use cases -This document describes the demo use cases for the `spp_farmer_registry_demo` module. -The demo is set in the **Philippines** context, showcasing farmer registry and -agricultural subsidy programs for smallholder farmers. +## Farm stories -## Table of Contents +Each farm is named by its family name and identified by an FM-code (FM1–FM8). Programs that target groups enroll the farm — not individual members. Multi-program enrollment is allowed when the farm satisfies more than one program's CEL. -1. [Overview](#overview) -2. [Philippines Context](#philippines-context) -3. [Demo Programs](#demo-programs) -4. [Demo Stories](#demo-stories) -5. [Logic Packs](#logic-packs) -6. [Use Cases by Audience](#use-cases-by-audience) -7. [Demo Scenarios](#demo-scenarios) -8. [Feature Demonstrations](#feature-demonstrations) +### Story 1: FM1 — Smallholder rice farmer, full lifecycle to graduation ---- - -## Overview - -The Farmer Registry Demo module provides realistic demo data that showcases OpenSPP's -capabilities for agricultural program management. It follows the "Fixed Stories + -Volume" architecture: - -- **Fixed Stories**: 8 named farmer personas with predefined farm profiles and program - journeys -- **GIS Data**: GPS coordinates and land parcel polygons across 8 Philippine provinces -- **Farm Cooperatives**: 2 cooperative personas demonstrating group hierarchy (group of - groups) -- **Edge Cases**: 3 additional personas for testing eligibility boundaries -- **Volume Data**: Random farm registrations with GIS coordinates for realistic map - views -- **Demo Programs**: 5 programs covering different agricultural subsidy scenarios -- **Logic Packs**: Pre-built CEL eligibility and benefit calculation rules +**Demonstration purpose:** End-to-end Input Subsidy lifecycle — enrolled, paid through three cycles, then graduated. Contrasts with FM2 who stays multi-enrolled and FM3 who continues active. Primary story for "graduation after target met". ---- - -## Philippines Context +**Program(s) the farm is enrolled in:** -The demo simulates a **Department of Agriculture (DA)** farmer support initiative in the -Philippines, targeting smallholder farmers across multiple provinces. +| Program | Reason for eligibility | Compliance | Status | +| -------------- | ------------------------------------------- | ------------------------------------------------------- | ----------- | +| Input Subsidy | smallholder (2.0 ha ≤ 5), has productive land | **Passed** each cycle — productive land share ≥ 50 % | **Exited** (graduated) | -### Setting +**Farm journey:** -| Attribute | Value | -| --------------------- | -------------------------------------------------------- | -| **Country** | Philippines | -| **Agency** | Department of Agriculture (DA) | -| **Target Population** | Smallholder farmers (≤5 hectares) | -| **Registry System** | Registry System for Basic Sectors in Agriculture (RSBSA) | -| **Currency** | Philippine Peso (PHP) | +1. Enrolled in Input Subsidy 150 days ago (rice, 2.0 ha, Cabanatuan area) +2. Payment #1 (₱200) — paid 120 days ago +3. Payment #2 (₱200) — paid 90 days ago +4. Payment #3 (₱200) — paid 60 days ago +5. Compliance pass each cycle (productive land = 100 % of total) +6. **Graduated 30 days ago** — target met, exited program -### Agricultural Context +**Existing change requests for the farm:** -- **Major crops**: Rice (palay), corn, coconut, sugarcane, vegetables -- **Livestock**: Carabao (water buffalo), goats, chickens, swine -- **Aquaculture**: Tilapia, milkfish (bangus), shrimp -- **Farm sizes**: Typically 0.5-5 hectares for smallholders -- **Seasons**: Wet season (June-November), Dry season (December-May) +- `update_farm_details` (approved) — Farm expanded to 3.0 ha after acquiring adjacent parcel -### Regions Represented - -| Persona | Province | Region | -| ------------------ | ------------- | ---------------------------- | -| Maria Santos | Nueva Ecija | Central Luzon (Region III) | -| Juan Dela Cruz | Pangasinan | Ilocos Region (Region I) | -| Rosa Garcia | Bukidnon | Northern Mindanao (Region X) | -| Amir Mangudadatu | Maguindanao | BARMM | -| Sofia Martinez | Laguna | CALABARZON (Region IV-A) | -| Ramon dela Cruz | Pampanga | Central Luzon (Region III) | -| Sittie Pangandaman | Lanao del Sur | BARMM | -| Danilo Villanueva | Davao del Sur | Davao Region (Region XI) | +**Geographical location:** Inland rice plains — Cabanatuan, Nueva Ecija --- -## Demo Programs - -### 1. Input Subsidy Program +### Story 2: FM2 — Multi-program mixed farmer -| Attribute | Value | -| ------------------- | ---------------------------------------------------------------- | -| **Target Type** | Households (Groups) | -| **Eligibility** | Smallholder (≤5 ha) with productive land | -| **Benefit Formula** | Base amount + (farm hectares x per-hectare rate) | -| **Example** | PHP 5,000 + (2.0 ha x PHP 2,500) = PHP 10,000 | -| **Stories** | Maria Santos, Juan Dela Cruz, Sofia Martinez, Sittie Pangandaman | +**Demonstration purpose:** A farm that satisfies more than one program's CEL simultaneously. Demonstrates concurrent enrollment, separate cycle/payment streams, and multi-CR sequencing on the same farm. Primary story for "multi-program coordination". -**Use Cases:** +**Program(s) the farm is enrolled in:** -- Rice and corn seed subsidy distribution -- Fertilizer assistance for smallholders -- Seasonal input support (wet/dry season) -- Per-hectare scaling of benefits +| Program | Reason for eligibility | Compliance | Status | +| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------ | -------- | +| Input Subsidy | smallholder (3.0 ha), has productive land (rice + vegetables) | **Passed** — productive land = 67 % of total | Enrolled | +| Livestock Support | livestock_count = 50 (chickens) > 0 | N/A (no compliance on this program) | Enrolled | -**Features Demonstrated:** +**Farm journey:** -- CEL-based eligibility evaluation -- Formula-based benefit calculation -- Group-based targeting -- Farm size-proportional entitlements +1. Enrolled in Input Subsidy 100 days ago (mixed farm: 1.5 ha rice + 0.5 ha vegetables + 50 chickens) +2. Payment #1 — Input Subsidy ₱250 — paid 70 days ago +3. Payment #2 — Input Subsidy ₱250 — paid 40 days ago +4. Enrolled in Livestock Support 80 days ago (chickens = 50 heads) +5. Payment #1 — Livestock Support ₱275 — paid 50 days ago +6. Both enrollments still active ---- - -### 2. Equipment Grant Program - -| Attribute | Value | -| --------------- | -------------------------------------------- | -| **Target Type** | Households (Groups) | -| **Eligibility** | Smallholder with 2+ years farming experience | -| **Benefit** | Fixed grant amount | -| **Stories** | Juan Dela Cruz, Sittie Pangandaman | - -**Use Cases:** +**Existing change requests for the farm:** -- Farm mechanization support (hand tractors, threshers) -- Post-harvest equipment grants -- Experience-based eligibility filtering -- One-time asset distribution +- `update_farm_details` (applied) — Expanded to 4.0 ha, added livestock area +- `manage_farm_activity` (pending) — Register new chicken-rearing activity (50 heads, subsistence) -**Features Demonstrated:** - -- Multi-criteria eligibility (size AND experience) -- Fixed-amount entitlements -- Edge case: new farmers excluded (experience < 2 years) +**Geographical location:** Inland mixed farming — San Pablo City, Laguna --- -### 3. Livestock Support Program +### Story 3: FM3 — Senior livestock farmer, gender + age diversity -| Attribute | Value | -| ------------------- | ----------------------------------------------- | -| **Target Type** | Households (Groups) | -| **Eligibility** | Farms with livestock activities | -| **Benefit Formula** | Base amount + (livestock count x per-head rate) | -| **Example** | PHP 3,750 + (20 heads x PHP 500) = PHP 13,750 | -| **Stories** | Rosa Garcia, Juan Dela Cruz, Danilo Villanueva | +**Demonstration purpose:** A senior female farmer whose primary income is livestock. Demonstrates non-cash-crop targeting and the per-head benefit formula. Contrasts with FM1's flat per-hectare payment. -**Use Cases:** +**Program(s) the farm is enrolled in:** -- Livestock dispersal programs (carabao, goat) -- Animal health and veterinary support -- Per-head benefit scaling -- Mixed farm support +| Program | Reason for eligibility | Compliance | Status | +| ------------------ | ------------------------------------ | ------------------------------------- | -------- | +| Livestock Support | livestock_count = 20 (goats) > 0 | N/A (no compliance on this program) | Enrolled | -**Features Demonstrated:** +**Farm journey:** -- Activity-based eligibility (livestock count > 0) -- Per-head benefit calculation -- Cross-activity farms (crops + livestock) +1. Enrolled in Livestock Support 120 days ago (mixed farm: 0.5 ha crops + 20 goats, 1.0 ha total) +2. Payment #1 — ₱275 (livestock_base 75 + 20 heads × ₱10) — paid 90 days ago +3. Payment #2 — ₱275 — paid 60 days ago +4. Payment #3 — ₱275 — paid 30 days ago +5. Active enrollment, not yet graduated ---- +**Existing change requests for the farm:** -### 4. Climate Resilience Program +- `update_farm_details` (approved) — Land tenure transferred to owner after inheritance -| Attribute | Value | -| --------------- | --------------------------------- | -| **Target Type** | Households (Groups) | -| **Eligibility** | Smallholder with idle/fallow land | -| **Benefit** | Fixed climate adaptation amount | -| **Stories** | Amir Mangudadatu | +**Geographical location:** Inland plateau, livestock area — Lipa City, Batangas -**Use Cases:** +--- -- Drought-affected farmer support -- Climate adaptation assistance -- Fallow land rehabilitation -- Emergency agricultural response +### Story 4: FM4 — Climate-vulnerable farmer with idle land -**Features Demonstrated:** +**Demonstration purpose:** A farmer whose declared idle land triggers Climate Resilience eligibility. Demonstrates the program's targeting logic (`farm_size_idle > 0`) and BARMM conflict-affected context. Contrasts with FM1/FM2 who satisfy productive-land programs only. -- Climate vulnerability targeting -- Idle land as eligibility indicator -- Fixed emergency-style benefits -- BARMM conflict/climate overlap scenarios +**Program(s) the farm is enrolled in:** ---- +| Program | Reason for eligibility | Compliance | Status | +| -------------------- | --------------------------------------------------------------- | ------------------------------------- | -------- | +| Climate Resilience | smallholder (4.0 ha), farm_size_idle = 1.0 ha > 0 | N/A (no compliance on this program) | Enrolled | -### 5. Aquaculture Support Program +**Farm journey:** -| Attribute | Value | -| --------------- | --------------------------------- | -| **Target Type** | Households (Groups) | -| **Eligibility** | Farms with aquaculture activities | -| **Benefit** | Fixed aquaculture support amount | -| **Stories** | Ramon dela Cruz | +1. Enrolled in Climate Resilience 55 days ago (vulnerability: very_high; 3.0 ha rice + 1.0 ha idle/fallow) +2. Payment #1 — ₱200 — paid 50 days ago +3. Payment #2 — ₱200 — paid 35 days ago +4. Active enrollment -**Use Cases:** +**Existing change requests for the farm:** -- Fishpond development support -- Fingerling and feed subsidy -- Aquaculture-specific targeting -- Non-crop farming support +- `update_farm_details` (rejected) — Request to reclassify productive area (1.5 ha crops, 2.5 ha idle); rejected pending field verification -**Features Demonstrated:** - -- Aquaculture activity detection -- Farm type differentiation -- Support for non-traditional farming +**Geographical location:** Inland BARMM — Cotabato City, Maguindanao --- -## Demo Stories - -### Maria Santos - The Rice Farmer +### Story 5: FM5 — Young female farmer, organic transition -**Profile:** +**Demonstration purpose:** Young female farmer in the highlands transitioning toward organic agriculture. Demonstrates the diversity dimension and Logic Pack–driven eligibility for early-career smallholders. -- 42-year-old female rice farmer -- 2 hectares in Nueva Ecija (rice granary of the Philippines) -- 10 years farming experience -- Smallholder, all land under crops +**Program(s) the farm is enrolled in:** -**Farm Data:** +| Program | Reason for eligibility | Compliance | Status | +| -------------- | -------------------------------------------- | ------------------------------------------------ | -------- | +| Input Subsidy | smallholder (2.0 ha), has productive land | **Passed** — productive land = 100 % of total | Enrolled | -| Attribute | Value | -| ----------- | ------------ | -| Farm Type | Crop | -| Total Size | 2.0 ha | -| Under Crops | 2.0 ha | -| Crops | Rice (palay) | -| Livestock | None | +**Farm journey:** -**Program Eligibility:** +1. Enrolled in Input Subsidy 70 days ago (vegetables + maize, 2.0 ha) +2. Payment #1 — ₱200 — paid 45 days ago +3. Active enrollment -- Input Subsidy: Eligible (smallholder + productive land) -- Equipment Grant: Eligible (10 years experience) -- Livestock Support: Not eligible (no livestock) +**Existing change requests for the farm:** -**Demo Points:** +- `manage_farm_activity` (draft) — Register organic vegetable cultivation (commercial, 0.5 ha) -- Typical Filipino rice farmer profile -- Female farmer representation -- Multi-program eligibility -- Productive smallholder success story +**Geographical location:** Mountain valley highlands — La Trinidad, Benguet --- -### Juan Dela Cruz - The Mixed Farmer +### Story 6: FM6 — Aquaculture, non-crop farming -**Profile:** +**Demonstration purpose:** Demonstrates that the registry handles non-crop farming. The farm is enrolled in Aquaculture Support — a program that targets a single field (`aquaculture_count > 0`) ignored by every other program. -- 45-year-old male mixed farmer -- 3 hectares in Pangasinan -- 15 years experience, crops + chickens -- Experienced and diversified +**Program(s) the farm is enrolled in:** -**Farm Data:** +| Program | Reason for eligibility | Compliance | Status | +| --------------------- | --------------------------------- | ------------------------------------- | -------- | +| Aquaculture Support | aquaculture_count > 0 (tilapia) | N/A (no compliance on this program) | Enrolled | -| Attribute | Value | -| --------------- | ---------------------- | -| Farm Type | Mixed | -| Total Size | 3.0 ha | -| Under Crops | 2.0 ha | -| Under Livestock | 1.0 ha | -| Crops | Rice, corn, vegetables | -| Livestock | 50 chickens | +**Farm journey:** -**Program Eligibility:** +1. Enrolled in Aquaculture Support 90 days ago (0.5 ha tilapia fishpond) +2. Payment #1 — ₱250 — paid 60 days ago +3. Payment #2 — ₱250 — paid 30 days ago +4. Active enrollment -- Input Subsidy: Eligible -- Equipment Grant: Eligible (15 years) -- Livestock Support: Eligible (50 heads) +**Existing change requests for the farm:** -**Demo Points:** +- `manage_farm_activity` (pending) — Update tilapia production (3,500 kg current, 4,000 kg expected) -- Diversified farm operations -- Eligible for multiple programs simultaneously -- Highest combined benefit potential -- Demonstrates cross-program coordination +**Geographical location:** Inland fishpond area — Dagupan, Pangasinan --- -### Rosa Garcia - The Livestock Farmer +### Story 7: FM7 — Equipment Grant + Input Subsidy stack -**Profile:** +**Demonstration purpose:** Young but experienced female farmer in BARMM. Qualifies for both Input Subsidy and Equipment Grant (12 years' experience clears the `experience_years >= 2` threshold). Demonstrates BARMM women in agriculture. -- 67-year-old female farmer -- 1 hectare in Bukidnon, Mindanao -- 8 years experience, goat farming -- Female-headed household +**Program(s) the farm is enrolled in:** -**Farm Data:** +| Program | Reason for eligibility | Compliance | Status | +| ---------------- | -------------------------------------------------------------------- | ------------------------------------------------------- | -------- | +| Input Subsidy | smallholder (1.5 ha), has productive land | **Passed** — productive land = 100 % of total | Enrolled | +| Equipment Grant | smallholder, experience_years 12 ≥ 2 | **Passed** — still smallholder, still has productive land | Enrolled | -| Attribute | Value | -| --------------- | ---------- | -| Farm Type | Mixed | -| Total Size | 1.0 ha | -| Under Crops | 0.5 ha | -| Under Livestock | 0.5 ha | -| Crops | Vegetables | -| Livestock | 20 goats | +**Farm journey:** -**Program Eligibility:** +1. Enrolled in Input Subsidy 130 days ago (rice + vegetables, 1.5 ha) +2. Payment #1 — Input Subsidy ₱175 — paid 100 days ago +3. Payment #2 — Input Subsidy ₱175 — paid 70 days ago +4. Enrolled in Equipment Grant 60 days ago +5. Payment #1 — Equipment Grant ₱500 — paid 30 days ago +6. Both enrollments active -- Input Subsidy: Eligible -- Equipment Grant: Eligible (8 years) -- Livestock Support: Eligible (20 heads) +**Existing change requests for the farm:** -**Demo Points:** +- `manage_farm_activity` (approved) — Register new maize cultivation for dry season (commercial, 0.8 ha) -- Senior female farmer -- Livestock-focused livelihood -- Small but productive farm -- Multi-program beneficiary +**Geographical location:** Inland BARMM — Marawi, Lanao del Sur --- -### Amir Mangudadatu - The Climate-Affected Farmer +### Story 8: FM8 — Threshold edge case at the smallholder boundary -**Profile:** +**Demonstration purpose:** Boundary-condition testing. The farm sits at the smallholder threshold (5.0 ha) with deep diversification. Demonstrates that the eligibility CEL evaluates correctly at the exact boundary and that highly experienced farmers (25 years) still qualify when other criteria fit. -- 50-year-old male crop farmer -- 4 hectares in Maguindanao (BARMM) -- 20 years experience, drought-affected -- 1 hectare idle/fallow land +**Program(s) the farm is enrolled in:** -**Farm Data:** +| Program | Reason for eligibility | Compliance | Status | +| ------------------ | --------------------------------------------------------- | ------------------------------------- | -------- | +| Livestock Support | livestock_count = 45 (15 cattle + 30 goats) > 0 | N/A (no compliance on this program) | Enrolled | -| Attribute | Value | -| ----------- | ------ | -| Farm Type | Crop | -| Total Size | 4.0 ha | -| Under Crops | 3.0 ha | -| Idle/Fallow | 1.0 ha | -| Crops | Rice | -| Livestock | None | +**Farm journey:** -**Program Eligibility:** +1. Enrolled in Livestock Support 180 days ago (3.0 ha crops + 2.0 ha livestock; 15 cattle + 30 goats) +2. Payment #1 — ₱275 + per-head bonus — paid 150 days ago +3. Payment #2 — ₱275 — paid 120 days ago +4. Active enrollment, sitting exactly at smallholder boundary -- Input Subsidy: Eligible -- Equipment Grant: Eligible (20 years) -- Climate Resilience: Eligible (idle land > 0) +**Existing change requests for the farm:** -**Demo Points:** +- `update_farm_details` (revision) — Update experience years (claimed 20) and land breakdown; revision requested for supporting documents -- Climate vulnerability scenario -- BARMM conflict-affected context -- Idle land as climate impact indicator -- Emergency program targeting +**Geographical location:** Inland highland plateau — Malaybalay, Bukidnon --- -### Sofia Martinez - The Organic Transition Farmer - -**Profile:** - -- 42-year-old female farmer -- 2 hectares in Laguna (CALABARZON) -- 5 years experience, transitioning to organic -- Growing vegetables and maize +## Edge case stories -**Farm Data:** +These three farms exist to demonstrate eligibility *rejection* paths. They are referenced by the rejection demo scenario; they are not enrolled in any program. -| Attribute | Value | -| ----------- | ----------------- | -| Farm Type | Crop | -| Total Size | 2.0 ha | -| Under Crops | 2.0 ha | -| Crops | Vegetables, maize | -| Livestock | None | +### Story 9: EC1 — Large commercial farm -**Program Eligibility:** +**Demonstration purpose:** A 50 ha commercial operation. Fails the `is_smallholder` check (smallholder threshold = 5 ha) so it's rejected from Input Subsidy, Equipment Grant, and Climate Resilience. Demonstrates targeting exclusion at the upper bound. -- Input Subsidy: Eligible -- Equipment Grant: Eligible (5 years) -- Livestock Support: Not eligible - -**Demo Points:** - -- Organic farming transition -- Near-urban agriculture (Laguna) -- Young female farmer -- Crop diversification +**Geographical location:** Background story — outside the smallholder envelope. --- -### Ramon dela Cruz - The Aquaculture Farmer - -**Profile:** - -- 35-year-old male aquaculture farmer -- 0.5 hectare fishpond in Pampanga -- 7 years experience, tilapia farming -- Leased land +### Story 10: EC2 — Idle-land farm with no productive land -**Farm Data:** +**Demonstration purpose:** A 3 ha farm where every hectare is idle. Fails `has_productive_land` (Input Subsidy compliance also fails on the same field). Demonstrates the difference between *having land* and *having productive land*. Eligible only for Climate Resilience because that program's CEL keys on `farm_size_idle > 0`. -| Attribute | Value | -| ----------------- | -------------------- | -| Farm Type | Aquaculture | -| Total Size | 0.5 ha | -| Under Aquaculture | 0.5 ha | -| Aquaculture | 1 fishpond (tilapia) | -| Crops | None | +**Geographical location:** Background story — climate-affected zone. -**Program Eligibility:** +--- -- Aquaculture Support: Eligible -- Input Subsidy: Not eligible (no crops) -- Livestock Support: Not eligible +### Story 11: EC3 — New farmer, less than two years' experience -**Demo Points:** +**Demonstration purpose:** A 2 ha farm with one year of experience. Eligible for Input Subsidy (the CEL ignores experience) but rejected from Equipment Grant (requires `experience_years >= 2`). Demonstrates the experience-based threshold. -- Non-crop farming representation -- Aquaculture-specific programs -- Small-scale fishpond operations -- Central Luzon aquaculture belt +**Geographical location:** Background story — eligible-with-caveats. --- -### Sittie Pangandaman - The Experienced Female Farmer - -**Profile:** - -- 32-year-old female farmer -- 1.5 hectares in Lanao del Sur (BARMM) -- 12 years experience -- Female-headed household - -**Farm Data:** +## Cooperative stories -| Attribute | Value | -| ----------- | ---------------- | -| Farm Type | Crop | -| Total Size | 1.5 ha | -| Under Crops | 1.5 ha | -| Crops | Rice, vegetables | -| Livestock | None | +Cooperatives in the demo are *groups of farms* — true group-of-groups hierarchy. They demonstrate that the registry supports federation structures and aggregated metrics over member farms. -**Program Eligibility:** +### Story 12: COOP1 — Nueva Ecija Rice Cooperative -- Input Subsidy: Eligible -- Equipment Grant: Eligible (12 years) -- Livestock Support: Not eligible +**Demonstration purpose:** A two-farm rice cooperative spanning Central Luzon. Aggregated farm size = 4.0 ha; combined eligibility behaves as the union of member-farm CELs. Demonstrates the group-of-groups data model and cooperative-level reporting (combined hectarage, member count). -**Demo Points:** +**Member farms:** FM1 (Maria Santos, Nueva Ecija) + FM5 (Sofia Martinez, Benguet). -- Young but experienced farmer -- BARMM women in agriculture -- Multiple crop types -- Female farmer empowerment +**Geographical location:** Central Luzon (Nueva Ecija, Benguet). --- -### Danilo Villanueva - The Commercial-Edge Farmer +### Story 13: COOP2 — BARMM Farmers Federation -**Profile:** +**Demonstration purpose:** A regional federation pooling two BARMM smallholder farms. Combined size 5.5 ha — *exceeds* the smallholder threshold individually. Demonstrates that program eligibility is computed per *member* farm, not on the federation aggregate (so each member is still treated as a smallholder). -- 38-year-old male mixed farmer -- 5 hectares in Davao del Sur (at smallholder threshold) -- 25 years experience, cattle and goats -- Edge case for program eligibility +**Member farms:** FM4 (Amir Mangudadatu, Maguindanao) + FM7 (Sittie Pangandaman, Lanao del Sur). -**Farm Data:** +**Geographical location:** BARMM (Maguindanao, Lanao del Sur). -| Attribute | Value | -| --------------- | ------------------------- | -| Farm Type | Mixed | -| Total Size | 5.0 ha | -| Under Crops | 3.0 ha | -| Under Livestock | 2.0 ha | -| Crops | Coconut, cacao | -| Livestock | 45 heads (cattle + goats) | +--- + +## Demo scenarios -**Program Eligibility:** +### Scenario 1: New enrollment -- Input Subsidy: Eligible (at 5 ha threshold) -- Equipment Grant: Eligible (25 years) -- Livestock Support: Eligible (45 heads) +Walk through enrolling a previously unregistered smallholder. -**Demo Points:** +1. Open Registry → Groups → New, set `is_group=true` and `is_farm=true` +2. Add the head member and key fields (farm_total_size, farm_size_under_crops, experience_years) +3. Open Programs → Input Subsidy → Verify Eligibility +4. The new farm is moved from `not_eligible` (or absent) to `enrolled` because the CEL now matches +5. Show the resulting cycle and the first scheduled payment -- Threshold/edge case testing -- Large smallholder at boundary -- Highly diversified farm -- Davao agricultural economy +**Key messages:** +- Eligibility is data-driven; changing the farm's facts changes the verdict +- The CEL evaluates on demand (Verify Eligibility) and at every cycle creation --- -## Farm Cooperative Personas (Group Hierarchy) +### Scenario 2: Multi-program coordination -Farm Cooperatives demonstrate the **group hierarchy** feature where a cooperative -(group) contains individual farms (groups) as members — a group of groups. +Show how a single farm fans out into two programs. -### Nueva Ecija Rice Cooperative +1. Open FM2 farm → see two memberships (Input Subsidy + Livestock Support) +2. Open Input Subsidy → Cycles → see FM2 in cycle 4 +3. Open Livestock Support → Cycles → see FM2 in cycle 3 with a different payment amount +4. Show that the two payment streams are independent (separate batches, separate journals) -**Profile:** +**Key messages:** +- Multi-program is a property of *fact pattern*, not configuration +- Each program owns its own cycle/entitlement workflow -- Rice farming cooperative in Nueva Ecija, Central Luzon -- Contains 2 member farms: Maria Santos + Sofia Martinez -- Combined area: 4.0 hectares -- All members are rice/crop farmers +--- -**Hierarchy:** +### Scenario 3: Compliance failure and graduation -``` -Nueva Ecija Rice Cooperative (Group) -├── Maria Santos Farm (Group) ─── 2.0 ha rice -└── Sofia Martinez Farm (Group) ── 2.0 ha vegetables, maize -``` +Use FM1 to demonstrate that a smallholder who keeps their productive land remains compliant; contrast with a hypothetical farm that abandons its productive land. -**Demo Points:** +1. Open Input Subsidy → compliance manager → show CEL `has_productive_land == true and farm_size_hectares > 0` +2. Open FM1 → cycle membership history → state `enrolled` for cycles 1–3, then `graduated` +3. Open a hypothetical FM-NULL with `farm_size_under_crops = 0` post-cycle → state `non_compliant` for that cycle, no entitlement generated -- Group of groups hierarchy -- Cooperative-level aggregated data (combined hectares, member count) -- Cooperative registration and management -- Member farm listing within cooperative view +**Key messages:** +- Eligibility gates *enrollment*; compliance gates *each cycle's payment* +- Compliance can fire on any field reachable from CEL; here we use the productive-land share --- -### BARMM Farmers Federation +### Scenario 4: Aquaculture targeting -**Profile:** +Demonstrate that the system handles non-crop farming. -- Federation of farms in Bangsamoro Autonomous Region (BARMM) -- Contains 2 member farms: Amir Mangudadatu + Sittie Pangandaman -- Combined area: 5.5 hectares (including 1 ha idle) -- Mixed crop types across members +1. Open FM6 farm → activities → 0.5 ha tilapia fishpond +2. Open Aquaculture Support program → CEL `aquaculture_count > 0` +3. Open the program's cycle → FM6 in cycle 4 with payment ₱250 +4. Verify Eligibility on FM1 (rice) — no change (FM1 is not eligible because `aquaculture_count == 0`) -**Hierarchy:** - -``` -BARMM Farmers Federation (Group) -├── Amir Mangudadatu Farm (Group) ──── 4.0 ha (3.0 crops + 1.0 idle) -└── Sittie Pangandaman Farm (Group) ── 1.5 ha crops -``` - -**Demo Points:** - -- Regional farmer federation -- BARMM-specific cooperative structures -- Federation exceeds smallholder threshold (5.5 ha combined) even though individual - members qualify -- Climate-affected member (Ibrahim) within a broader federation +**Key messages:** +- Programs can target specific livelihood types via CEL +- The same farm record carries multiple livelihoods (mixed farms enroll into multiple programs, single-livelihood farms into one) --- -## Edge Case Personas +### Scenario 5: Climate Resilience for idle land -### AgriCorp Holdings - Large Commercial Farm +Show how `farm_size_idle` becomes a positive signal for climate-vulnerable households. -| Attribute | Value | -| --------------- | -------------------------------------- | -| Farm Size | 50 ha | -| Is Smallholder | No | -| Expected Result | Rejected from all smallholder programs | +1. Open Climate Resilience program → CEL `is_smallholder and farm_size_idle > 0` +2. Open FM4 → 3 ha rice + 1 ha idle = 4 ha total → matches CEL +3. Show the cycle and 2 paid payments (₱200 each) +4. Contrast with EC1 (50 ha, idle) → fails `is_smallholder` even though `farm_size_idle > 0` -**Demo Point:** Demonstrates proper targeting exclusion for large commercial operations. +**Key messages:** +- Idle land isn't always a negative — Climate Resilience treats it as a vulnerability signal +- CEL composition (AND) eliminates large commercial farms from a program targeting smallholders --- -### Idle Land Farm - No Productive Land - -| Attribute | Value | -| ------------------- | ------------------------------------------------------------ | -| Farm Size | 3 ha (all idle/fallow) | -| Has Productive Land | No | -| Expected Result | Rejected from Input Subsidy, eligible for Climate Resilience | +### Scenario 6: Eligibility rejection paths -**Demo Point:** Tests edge case where land exists but isn't productive. +Show that the engine correctly excludes farms that look eligible at a glance. ---- - -### New Farmer - No Experience +1. EC1 (50 ha commercial) → rejected from Input Subsidy / Equipment Grant / Climate Resilience (fails `is_smallholder`) +2. EC2 (3 ha all idle) → rejected from Input Subsidy (fails `has_productive_land`) +3. EC3 (2 ha, 1 year experience) → eligible for Input Subsidy; rejected from Equipment Grant (fails `experience_years >= 2`) -| Attribute | Value | -| --------------- | --------------------------------------------------------- | -| Farm Size | 2 ha | -| Experience | 1 year | -| Expected Result | Eligible for Input Subsidy, rejected from Equipment Grant | - -**Demo Point:** Tests experience-based eligibility threshold. +**Key messages:** +- Each program's CEL is independent — rejecting one doesn't reject all +- Edge cases drive the test matrix; volume seeding produces farms across the same boundaries --- -## Logic Packs - -Pre-built logic packages using CEL expressions for program eligibility and benefit -calculations. +### Scenario 7: Cooperative as group of groups -### Pack 1: Input Subsidy Program +Demonstrate the group-of-groups data model. -| Item | Type | CEL Expression | -| ------------------------- | ------- | ----------------------------------------------------------------- | -| Smallholder Eligibility | Filter | `is_smallholder && has_productive_land` | -| Input Subsidy Calculation | Formula | `input_subsidy_base + (farm_size_hectares * per_hectare_subsidy)` | +1. Open COOP1 (Nueva Ecija Rice Cooperative) → see member farms FM1 + FM5 +2. Show aggregated metrics — combined 4.0 ha, 2 member farms +3. Open FM1 → see cooperative membership (FM1 belongs to COOP1) +4. Run Verify Eligibility on Input Subsidy — eligibility is computed per member farm; the cooperative itself is not a program target -### Pack 2: Equipment Grant Program +**Key messages:** +- Cooperatives are organisational records; programs target the underlying farms +- Federations can pool farms from different provinces/regions; the registry preserves the geographic data of each member -| Item | Type | CEL Expression | -| ------------------------------ | ------- | ----------------------------------------- | -| Experienced Farmer Eligibility | Filter | `is_smallholder && experience_years >= 2` | -| Equipment Grant Amount | Formula | `equipment_grant_amount` | - -### Pack 3: Livestock Support Program - -| Item | Type | CEL Expression | -| ----------------------------- | ------- | ------------------------------------------------------ | -| Livestock Farmer Eligibility | Filter | `livestock_count > 0` | -| Livestock Support Calculation | Formula | `livestock_base + (livestock_count * per_head_amount)` | +--- -### Pack 4: Climate Resilience Program +### Scenario 8: Change request lifecycle -| Item | Type | CEL Expression | -| --------------------------------- | ------- | -------------------------------------- | -| Climate Vulnerability Eligibility | Filter | `is_smallholder && farm_size_idle > 0` | -| Climate Adaptation Amount | Formula | `climate_adaptation_amount` | +Walk through the 9 demo CRs to show every CR state. -### Pack 5: Aquaculture Support Program +1. Approved: FM1 `update_farm_details` — farm expanded after acquisition +2. Applied: FM2 `update_farm_details` — added livestock area, applied automatically +3. Pending: FM2 `manage_farm_activity` — register chicken activity (awaiting validator) +4. Approved: FM3 `update_farm_details` — land tenure transfer +5. Draft: FM5 `manage_farm_activity` — register organic crop (UI workflow stage) +6. Pending: FM6 `manage_farm_activity` — update tilapia yield +7. Rejected: FM4 `update_farm_details` — reclassify idle land, rejected pending verification +8. Approved: FM7 `manage_farm_activity` — register dry-season maize +9. Revision: FM8 `update_farm_details` — experience claim flagged for documentation -| Item | Type | CEL Expression | -| ------------------------------ | ------- | ---------------------------- | -| Aquaculture Farmer Eligibility | Filter | `aquaculture_count > 0` | -| Aquaculture Support Amount | Formula | `aquaculture_support_amount` | +**Key messages:** +- The demo covers 6 CR states (Draft, Pending, Approved, Applied, Rejected, Revision) +- Two CR types in scope (`update_farm_details` and `manage_farm_activity`); additional types from `spp_farmer_registry_cr` are wired but not seeded by the demo --- -## Use Cases by Audience +### Scenario 9: Approval workflow on cycles + entitlements -### For Sales Demos +Demonstrate that demo programs route cycles and entitlements through the approval workflow (a feature MIS demo lacks). -**Quick Demo (15 minutes):** +1. Open Input Subsidy → Cycles → click "New Cycle" +2. The cycle enters state `to_approve` (not `draft`) because its cycle manager has `approval_definition_id` set +3. Show the approval review record — assigned to `group_programs_manager`, SLA 3 days +4. As Program Manager, approve the cycle → state moves to `approved` +5. Generate entitlements → each entitlement enters `pending_validation` and follows the same approval flow -1. Show farmer registry dashboard with farm statistics -2. Navigate to Maria Santos - show farm profile and eligibility -3. Show Juan Dela Cruz - demonstrate multi-program eligibility -4. Highlight aquaculture support (Ramon dela Cruz) for non-crop farming +**Key messages:** +- Approval is opt-in per program manager; here every farmer demo program has it wired +- Adding `manager.approval_definition_id` is the only knob — the rest is the standard `spp.approval.definition` framework -**Comprehensive Demo (45 minutes):** +--- -1. Farm registration workflow -2. Farm details and activity management -3. Agricultural season setup -4. Program eligibility evaluation using Logic Packs -5. Benefit calculation demonstration -6. Change request for farm data updates -7. Dashboard and reporting +## References -### For Training +### Constellation of included registrants -**Registry Officer Training:** +The farmer demo currently supports a single locale (`fil_PH`). Locale-specific name pools live in `seeded_farm_generator.py`; the per-story names below are the actual values written by `farmer_demo_generator.py`. -- Use all 8 personas to explain farm registration -- Demonstrate farm details entry (classification, acreage, experience) -- Practice adding farm activities (crops, livestock, aquaculture) -- Create agricultural seasons +#### Farms used in stories -**Program Officer Training:** +| Code | Filipino name | Story angle | +| ---- | ------------------ | ---------------------------------------- | +| FM1 | Santos | Smallholder graduation | +| FM2 | Dela Cruz | Multi-program mixed | +| FM3 | Garcia | Senior livestock | +| FM4 | Mangudadatu | Climate Resilience / idle land | +| FM5 | Martinez | Young female / organic transition | +| FM6 | Dela Cruz (fishpond) | Aquaculture | +| FM7 | Pangandaman | Multi-program (Input + Equipment) | +| FM8 | Villanueva | Smallholder boundary edge case | +| EC1 | (volume-generated) | Large commercial — rejection | +| EC2 | (volume-generated) | Idle-only — rejection | +| EC3 | (volume-generated) | Inexperienced — partial rejection | +| COOP1| Nueva Ecija Rice Cooperative | Cooperative (FM1 + FM5) | +| COOP2| BARMM Farmers Federation | Federation (FM4 + FM7) | -- Use Logic Packs to explain eligibility rules -- Walk through benefit calculations with concrete examples -- Demonstrate edge cases (large farm rejection, new farmer exclusion) -- Practice multi-program enrollment +#### Farm information -**Change Request Training:** +##### Information about FM1 -- Submit farm detail updates via change request -- Add new farm activities through CR workflow -- Practice approval/rejection workflows +| Code | Filipino | +| ---- | -------- | +| FM1 | Santos | -### For Testing +| | Geographic location | +| ----------- | ---------------------------- | +| Filipino | Cabanatuan City, Nueva Ecija | -**Eligibility Testing:** +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | -------------- | +| FM1M1 | Head | 42 | Female | Maria Santos | -- 8 eligible personas with known expected results -- 3 edge case personas for boundary testing -- Each Logic Pack has clear input/output expectations +**Farm facts:** 2.0 ha rice; experience 10 years; productive land 100 %. -**Regression Testing:** +##### Information about FM2 -- Fixed personas ensure consistent test data -- CEL expressions can be validated against expected outcomes -- Farm data provides diverse test scenarios +| Code | Filipino | +| ---- | --------- | +| FM2 | Dela Cruz | ---- +| | Geographic location | +| ----------- | ---------------------------- | +| Filipino | San Pablo City, Laguna | -## Demo Scenarios +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | --------------- | +| FM2M1 | Head | 45 | Male | Juan Dela Cruz | -### Scenario 1: Farm Registration and Program Enrollment +**Farm facts:** 3.0 ha mixed (1.5 rice + 0.5 vegetables + 50 chickens on 1.0 ha livestock); experience 15 years. -**Objective:** Show end-to-end farmer registration to program enrollment +##### Information about FM3 -**Steps:** +| Code | Filipino | +| ---- | -------- | +| FM3 | Garcia | -1. Open farmer registry list view -2. Create new farm registration (or open Maria Santos) -3. Fill in farm details (type, size, classification) -4. Add farm activities (crops grown, livestock held) -5. Navigate to programs and check eligibility -6. Enroll in Input Subsidy Program -7. Show calculated benefit amount +| | Geographic location | +| ----------- | ----------------------- | +| Filipino | Lipa City, Batangas | -**Key Messages:** +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | ------------ | +| FM3M1 | Head | 67 | Female | Rosa Garcia | -- Streamlined farm registration process -- Automatic eligibility determination -- Transparent benefit calculation +**Farm facts:** 1.0 ha mixed (0.5 ha crops + 0.5 ha livestock with 20 goats); experience 5 years. ---- +##### Information about FM4 -### Scenario 2: Multi-Program Eligibility +| Code | Filipino | +| ---- | ----------- | +| FM4 | Mangudadatu | -**Objective:** Demonstrate how one farmer can qualify for multiple programs +| | Geographic location | +| ----------- | --------------------------------- | +| Filipino | Cotabato City, Maguindanao (BARMM) | -**Steps:** +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | ------------------ | +| FM4M1 | Head | 50 | Male | Amir Mangudadatu | -1. Open Juan Dela Cruz profile -2. Show farm data: 3 ha mixed farm, 15 years experience, 50 chickens -3. Check Input Subsidy eligibility: smallholder + productive land = eligible -4. Check Equipment Grant eligibility: smallholder + 15 years = eligible -5. Check Livestock Support eligibility: 50 livestock heads = eligible -6. Show consolidated benefit summary +**Farm facts:** 4.0 ha total (3.0 ha rice + 1.0 ha idle/fallow); experience 20 years; vulnerability `very_high`. -**Key Messages:** +##### Information about FM5 -- Holistic farmer support -- Multiple program coordination -- No duplicate registration needed +| Code | Filipino | +| ---- | -------- | +| FM5 | Martinez | ---- +| | Geographic location | +| ----------- | ----------------------------- | +| Filipino | La Trinidad, Benguet (highlands) | -### Scenario 3: Eligibility Edge Cases +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | ---------------- | +| FM5M1 | Head | 42 | Female | Sofia Martinez | -**Objective:** Show how the system correctly handles boundary conditions +**Farm facts:** 2.0 ha vegetables + maize; experience 5 years; organic transition in progress. -**Steps:** +##### Information about FM6 -1. Open "New Farmer" persona (1 year experience) -2. Show Input Subsidy: eligible (smallholder + productive land) -3. Show Equipment Grant: rejected (experience < 2 years) -4. Open "AgriCorp Holdings" (50 ha) -5. Show all programs: rejected (not smallholder) -6. Open "Idle Land Farm" (all fallow) -7. Show Input Subsidy: rejected (no productive land) -8. Show Climate Resilience: eligible (idle land > 0) +| Code | Filipino | +| ---- | --------- | +| FM6 | Dela Cruz | -**Key Messages:** +| | Geographic location | +| ----------- | ------------------------ | +| Filipino | Dagupan, Pangasinan | -- Transparent and auditable eligibility rules -- Proper targeting prevents leakage -- Edge cases handled correctly +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | ----------------- | +| FM6M1 | Head | 35 | Male | Ramon dela Cruz | ---- +**Farm facts:** 0.5 ha tilapia fishpond; experience 7 years. -### Scenario 4: Agricultural Season Management +##### Information about FM7 -**Objective:** Demonstrate seasonal farm activity tracking +| Code | Filipino | +| ---- | ----------- | +| FM7 | Pangandaman | -**Steps:** +| | Geographic location | +| ----------- | ------------------------------------ | +| Filipino | Marawi, Lanao del Sur (BARMM) | -1. Navigate to Seasons configuration -2. Create a new wet season (June-November) -3. Open a farm and add seasonal activities -4. Show activity types: planting, harvesting, inputs applied -5. Close season and review summary +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | -------------------- | +| FM7M1 | Head | 32 | Female | Sittie Pangandaman | -**Key Messages:** +**Farm facts:** 1.5 ha rice + vegetables; experience 12 years. -- Temporal tracking of farm activities -- Season-based program cycles -- Historical data for trend analysis +##### Information about FM8 ---- - -### Scenario 5: Farm Data Change Request - -**Objective:** Show the change request workflow for farm updates - -**Steps:** +| Code | Filipino | +| ---- | ---------- | +| FM8 | Villanueva | -1. Open a farmer profile -2. Submit a change request to update farm size -3. Show the CR workflow (draft → pending → validated → applied) -4. Verify updated farm details after approval -5. Show audit trail of the change +| | Geographic location | +| ----------- | ----------------------------- | +| Filipino | Malaybalay, Bukidnon | -**Key Messages:** +| Member ID | Role | Age | Gender | Filipino | +| --------- | ---- | --- | ------ | -------------------- | +| FM8M1 | Head | 38 | Male | Danilo Villanueva | -- Data integrity through approval workflows -- Complete audit trail -- Controlled updates to farm records +**Farm facts:** 5.0 ha mixed (3.0 ha crops + 2.0 ha livestock; 15 cattle + 30 goats); experience 25 years. Sits exactly at the smallholder boundary. --- -### Scenario 6: Farm Cooperative (Group Hierarchy) - -**Objective:** Demonstrate group of groups hierarchy for farmer cooperatives - -**Steps:** - -1. Open the Nueva Ecija Rice Cooperative profile -2. Show it is a group with `allow_all_member_type` enabled -3. Navigate to the Members tab — show member farms (Maria Santos, Sofia Martinez) -4. Click into Maria Santos farm — show it is itself a group with individual members -5. Return to cooperative level — show aggregated data (combined hectares) -6. Open BARMM Farmers Federation — show federation-level view -7. Demonstrate that the federation exceeds smallholder threshold (5.5 ha) while - individual members qualify - -**Key Messages:** - -- Cooperatives are represented as groups containing farm groups -- Multi-level hierarchy: Cooperative → Farm → Individual members -- Aggregated statistics at cooperative level -- Individual farm eligibility preserved within cooperative structure +### Configuration of included programs + +#### 1. Input Subsidy Program (Group) + +| Field | Value | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL (Eligibility) | `r.is_group == true and is_smallholder and has_productive_land` | +| CEL (Compliance) | `has_productive_land == true and farm_size_hectares > 0` | +| Constants | `input_subsidy_base` = 100; `per_hectare_subsidy` = 50 | +| Entitlement | base + (farm_size_hectares × per_hectare_subsidy) — e.g. 100 + (2.0 × 50) = ₱200 | +| Cycle | 30 days | +| Logic Pack | `farmer_input_subsidy` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | +| Compliance Note | Re-checks productive land each cycle. A farm that abandons productive use becomes `non_compliant` for that cycle and gets no entitlement. | + +#### 2. Equipment Grant Program (Group) + +| Field | Value | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL (Eligibility) | `r.is_group == true and is_smallholder and experience_years >= 2` | +| CEL (Compliance) | `is_smallholder == true and has_productive_land == true` | +| Constants | `equipment_grant_amount` = 500 | +| Entitlement | ₱500 fixed | +| Cycle | 30 days | +| Logic Pack | `farmer_equipment_grant` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | +| Compliance Note | A recipient who grows past the smallholder threshold or stops actively farming fails compliance and stops receiving disbursements. | + +#### 3. Livestock Support Program (Group) + +| Field | Value | +| ----------- | -------------------------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL | `r.is_group == true and livestock_count > 0` | +| Constants | `livestock_base` = 75; `per_head_amount` = 10 | +| Entitlement | base + (livestock_count × per_head_amount) — e.g. 75 + (20 × 10) = ₱275 | +| Cycle | 30 days | +| Logic Pack | `farmer_livestock_support` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | + +#### 4. Climate Resilience Program (Group) + +| Field | Value | +| ----------- | -------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL | `r.is_group == true and is_smallholder and farm_size_idle > 0` | +| Constants | `climate_adaptation_amount` = 200 | +| Entitlement | ₱200 fixed | +| Cycle | 30 days | +| Logic Pack | `farmer_climate_resilience` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | + +#### 5. Aquaculture Support Program (Group) + +| Field | Value | +| ----------- | -------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL | `r.is_group == true and aquaculture_count > 0` | +| Constants | `aquaculture_amount` = 250 | +| Entitlement | ₱250 fixed | +| Cycle | 30 days | +| Logic Pack | `farmer_aquaculture_support` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | --- -### Scenario 7: GIS Farm Mapping +### Overview of included change requests -**Objective:** Demonstrate geospatial visualization of farm locations and land parcels +| # | Type | Target | Registrant | State | Life event | +| --- | ---------------------- | ------ | ---------- | -------- | ----------------------------------------------------------- | +| 1 | Update farm details | Farm | FM1 | Approved | Farm expanded to 3.0 ha after acquiring adjacent parcel | +| 2 | Update farm details | Farm | FM2 | Applied | Expanded to 4.0 ha, added livestock area | +| 3 | Manage farm activity | Farm | FM2 | Pending | Register new chicken-rearing activity (50 heads) | +| 4 | Update farm details | Farm | FM3 | Approved | Land tenure transferred to owner after inheritance | +| 5 | Manage farm activity | Farm | FM5 | Draft | Register organic vegetable cultivation (commercial) | +| 6 | Manage farm activity | Farm | FM6 | Pending | Update tilapia production (3,500 kg current) | +| 7 | Update farm details | Farm | FM4 | Rejected | Reclassify productive area; rejected pending field check | +| 8 | Manage farm activity | Farm | FM7 | Approved | Register dry-season maize cultivation | +| 9 | Update farm details | Farm | FM8 | Revision | Experience claim flagged for supporting documents | -**Steps:** +**CR types covered:** `update_farm_details`, `manage_farm_activity`. Additional types from `spp_farmer_registry_cr` (e.g. `manage_land_parcel`, `manage_farm_asset`) are wired into the module but not seeded by the demo today. -1. Navigate to the GIS Map view of the farmer registry -2. View all 8 story farms plotted on the Philippine map -3. Zoom into the Nueva Ecija cluster (Maria Santos + Sofia Martinez area) -4. Click a farm marker to see farm details (name, size, type) -5. View the land parcel polygon for Santos Farm (2 ha rice paddy) -6. Zoom out to see the geographic distribution across Luzon, Visayas, and Mindanao -7. Compare BARMM farms (Mangudadatu, Pangandaman) vs Luzon farms -8. Navigate to a farm's Land Records tab to view parcel boundaries and land use +**CR states covered:** Draft, Pending, Approved, Applied (auto-applied on approval for some CR types), Rejected, Revision. -**Key Messages:** +--- -- Every farm has GPS coordinates and land parcel boundaries -- Map view for geographic planning and disaster response -- Land use classification on each parcel -- Spatial queries possible (e.g., "find all farms within 50km of a typhoon path") +### Geographic distribution -**Geographic Coverage:** +Each story farm is assigned to an administrative area appropriate to its locale from the demo area data. -| Persona | Region | Province | Coordinates | -| ------------------ | ----------------- | ------------- | ----------------- | -| Maria Santos | Central Luzon | Nueva Ecija | 15.59°N, 120.97°E | -| Juan Dela Cruz | Southern Luzon | Laguna | 14.27°N, 121.41°E | -| Rosa Garcia | Southern Luzon | Batangas | 13.76°N, 121.06°E | -| Sofia Martinez | Cordillera | Benguet | 16.40°N, 120.60°E | -| Ramon dela Cruz | Ilocos | Pangasinan | 16.02°N, 120.22°E | -| Amir Mangudadatu | BARMM | Maguindanao | 7.05°N, 124.85°E | -| Sittie Pangandaman | BARMM | Lanao del Sur | 7.90°N, 124.29°E | -| Danilo Villanueva | Northern Mindanao | Bukidnon | 8.05°N, 125.05°E | +| Character | Code | Filipino location | +| ---------------------------------------- | ---- | ------------------------------- | +| Inland rice plains | FM1 | Cabanatuan City, Nueva Ecija | +| Inland mixed farming | FM2 | San Pablo City, Laguna | +| Inland plateau, livestock area | FM3 | Lipa City, Batangas | +| Inland BARMM, conflict-affected | FM4 | Cotabato City, Maguindanao | +| Mountain valley highlands | FM5 | La Trinidad, Benguet | +| Inland fishpond area | FM6 | Dagupan, Pangasinan | +| Inland BARMM, women-in-agriculture | FM7 | Marawi, Lanao del Sur | +| Inland highland plateau, threshold edge | FM8 | Malaybalay, Bukidnon | +| Cooperative — Central Luzon | COOP1| Nueva Ecija + Benguet | +| Cooperative — BARMM federation | COOP2| Maguindanao + Lanao del Sur | --- -## Feature Demonstrations - -### Farm Registry Features - -| Feature | Demo Persona | Description | -| ----------------- | ---------------------------- | ---------------------------------------- | -| Crop farming | Maria Santos | Pure rice farming profile | -| Mixed farming | Juan Dela Cruz | Crops + livestock combination | -| Aquaculture | Ramon dela Cruz | Fishpond operations | -| Female farmers | Maria, Rosa, Sofia, Sittie | Gender-disaggregated data | -| Climate impact | Amir Mangudadatu | Idle/fallow land tracking | -| Edge threshold | Danilo Villanueva | At 5 ha smallholder boundary | -| Farm cooperative | Nueva Ecija Rice Cooperative | Group of groups hierarchy | -| Farmer federation | BARMM Farmers Federation | Regional multi-farm federation | -| GIS mapping | All 8 personas | GPS coordinates across 8 provinces | -| Land parcels | All 8 personas | Land records with polygon boundaries | -| Land use | All 8 personas | Cultivation, pasture, aquaculture, mixed | - -### Program Features - -| Feature | Demo Program | Demo Persona | -| ----------------- | ------------------- | -------------------------- | -| CEL eligibility | Input Subsidy | All personas | -| Formula benefits | Input Subsidy | Maria Santos (per-hectare) | -| Fixed benefits | Equipment Grant | Juan Dela Cruz | -| Per-head scaling | Livestock Support | Rosa Garcia (20 goats) | -| Activity-based | Aquaculture Support | Ramon dela Cruz | -| Climate targeting | Climate Resilience | Amir Mangudadatu | - -### Change Request Features - -| Feature | CR Type | Description | -| -------------------- | ---------------- | ---------------------------------- | -| Update farm details | Farm Details CR | Change farm size, classification | -| Add farm activity | Farm Activity CR | Add new crop or livestock activity | -| Update farm activity | Farm Activity CR | Modify existing activity details | +### Seeded volume blueprints + +In addition to the named stories, `seeded_farm_generator.py` produces ~730 volume farms across 21 deterministic blueprints. Counts and shapes are seed-stable (`seed=42`). + +| Blueprint | Count | Zone | Type | Size (ha) | Experience | Head gender | Members | +| ------------------------------------------- | ----- | ----------- | ----------- | --------- | ---------- | ----------- | ------- | +| Small female rice farmer | 40 | rural | crop | 1.0–2.0 | 5–15 | F | 2 | +| Small male rice farmer | 45 | rural | crop | 1.0–3.0 | 3–20 | M | 3 | +| Small maize farmer | 35 | rural | crop | 1.0–2.5 | 4–18 | M | 2 | +| Small female vegetable farmer | 30 | peri-urban | crop | 0.5–1.5 | 2–12 | F | 2 | +| Rice + vegetable farmer | 35 | rural | crop | 2.0–4.0 | 8–25 | M | 4 | +| Highland crop farmer (Cordillera) | 25 | rural | crop | 1.0–2.0 | 5–20 | M | 3 | +| Mixed rice + chicken | 35 | rural | mixed | 2.0–3.5 | 5–20 | M | 3 | +| Mixed maize + goat | 30 | rural | mixed | 1.5–3.0 | 4–18 | M | 2 | +| Female-headed mixed rice + cattle | 30 | rural | mixed | 2.0–4.0 | 8–22 | F | 3 | +| Mixed vegetable + chicken (peri-urban) | 35 | peri-urban | mixed | 1.0–2.0 | 3–15 | F | 2 | +| Goat farmer | 30 | rural | livestock | 1.0–2.5 | 5–20 | M | 2 | +| Cattle rancher | 25 | rural | livestock | 3.0–6.0 | 10–30 | M | 3 | +| Female chicken farmer | 35 | peri-urban | livestock | 0.5–1.5 | 2–12 | F | 1 | +| Fishpond farmer (tilapia) | 30 | rural | aquaculture | 0.5–2.0 | 3–15 | M | 2 | +| Mixed fishpond + rice | 25 | rural | mixed | 1.0–3.0 | 5–18 | M | 3 | +| Large commercial crop | 25 | rural | crop | 5.0–10.0 | 15–35 | M | 4 | +| Large mixed commercial | 25 | rural | mixed | 5.0–8.0 | 12–30 | M | 3 | +| Drought-affected (idle land) | 30 | rural | crop | 2.0–4.0 | 8–25 | M | 3 | +| Flood-affected female farmer | 25 | rural | crop | 1.0–2.5 | 5–18 | F | 2 | +| Young farmer (< 3 years' experience) | 40 | rural | crop | 0.5–2.0 | 0–3 | any | 1 | +| Elderly farmer (20+ years' experience) | 30 | rural | crop | 1.0–3.0 | 20–40 | M | 2 | + +**Total blueprints:** 21. **Total farms:** ~730. **Estimated members:** ~1,500. --- -## Appendix: Data Generation Order - -For optimal demo setup: - -1. **First:** Install `spp_farmer_registry_demo` module (installs all dependencies) -2. **Second:** Run the Farmer Demo Wizard to generate demo data -3. **Third:** Verify farm registrations and program enrollments - -### Prerequisites - -The following modules are auto-installed as dependencies: - -- `spp_starter_farmer_registry` - Core farmer registry modules -- `spp_farmer_registry_cr` - Change request types for farm data -- `spp_demo` - Demo infrastructure -- `spp_studio` - Logic Studio for Logic Packs -- `spp_registry_group_hierarchy` - Group hierarchy support +### Overview + +| Metric | Count | +| ------------------------ | --------------------------------------------------------------------------- | +| Story farms | 8 (FM1–FM8) | +| Edge-case stories | 3 (EC1–EC3) | +| Cooperative stories | 2 (COOP1, COOP2) | +| Total programs included | 5 | +| Programs with compliance | 2 (Input Subsidy, Equipment Grant) | +| Change requests | 9 (2 types, 6 states) | +| Demo scenarios | 9 | +| Locales | 1 (`fil_PH`) | +| Seeded volume | ~730 farms, ~1,500 individuals | +| Approval definitions | Cycle + Entitlement (Program Manager, 3-day SLA) on every demo program | diff --git a/spp_farmer_registry_demo/models/demo_programs.py b/spp_farmer_registry_demo/models/demo_programs.py index db9b1b0bf..f18a42bb2 100644 --- a/spp_farmer_registry_demo/models/demo_programs.py +++ b/spp_farmer_registry_demo/models/demo_programs.py @@ -29,6 +29,12 @@ "cycle_duration": 1, "rrule_type": "monthly", "cel_expression": "r.is_group == true and is_smallholder and has_productive_land", + # Compliance: a beneficiary must still operate productive land at the + # start of each cycle. Lets a farm fall non-compliant mid-program if + # they stop farming productively (admin marks the farm inactive, all + # productive segments drop to 0, etc.) — re-evaluation moves them out + # of the next cycle without a manual de-enrollment. + "compliance_cel_expression": "has_productive_land == true and farm_size_hectares > 0", "logic_pack": "farmer_input_subsidy", "use_logic_studio": True, "logic_name": "Smallholder Eligibility", @@ -43,6 +49,7 @@ "Per-hectare benefit scaling", "Smallholder targeting", "CEL: is_smallholder and has_productive_land", + "Compliance: ongoing productive-land check", "Logic Pack: farmer_input_subsidy", ], }, @@ -58,6 +65,11 @@ "cycle_duration": 1, "rrule_type": "monthly", "cel_expression": "r.is_group == true and is_smallholder and experience_years >= 2", + # Compliance: the equipment grant is targeted at smallholders with + # productive land. If the recipient grows past the smallholder + # threshold or abandons productive land, they should stop receiving + # disbursements. Re-evaluated per cycle. + "compliance_cel_expression": "is_smallholder == true and has_productive_land == true", "logic_pack": "farmer_equipment_grant", "use_logic_studio": True, "logic_name": "Experienced Farmer Eligibility", @@ -67,6 +79,7 @@ "Experience-based eligibility", "Fixed grant amount", "Multi-criteria targeting", + "Compliance: must remain smallholder with productive land", "Logic Pack: farmer_equipment_grant", ], }, diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index b0cf602a5..6fc1418c3 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -1271,8 +1271,149 @@ def _create_program_via_wizard(self, program_def): cycle_manager.interval, ) + # Configure the eligibility manager's CEL expression so the engine + # uses the program's actual rule when admins re-evaluate eligibility. + # Without this the manager has no CEL set, so re-evaluation re-classifies + # seeded enrollments as Not Eligible even when the blueprint flags + # said they qualify. Mirrors the MIS fix from commit aa6da89d. + self._configure_eligibility_manager(program, program_def) + + # Same wiring for compliance — programs that ship a + # `compliance_cel_expression` (Input Subsidy, Equipment Grant) need + # their compliance manager's CEL set so disbursements actually flow + # through the compliance workflow. See OP#915. + self._configure_compliance_manager(program, program_def) + + # Wire approval definitions onto the cycle + entitlement managers so + # cycles and entitlements created on this program enter the demo's + # approval workflow. Without this the managers' `approval_definition_id` + # stays empty and the records auto-approve, which hides the workflow + # from anyone exercising the demo. See OP#915. + self._configure_program_approvals(program) + return program + def _configure_eligibility_manager(self, program, program_def): + """Configure the eligibility manager with the program's CEL expression. + + Eligibility uses ``program.eligibility_manager_ids`` (m2m of wrappers); + each wrapper points to the concrete manager via ``manager_ref_id``. + We write ``eligibility_mode='cel'`` + ``cel_expression`` on the first + wrapper that supports CEL (existing definitions on subsequent wrappers + are left alone). Mirrors ``MisDemoGenerator._configure_eligibility_manager``. + """ + try: + cel_expression = program_def.get("cel_expression") + if not cel_expression: + return + + for wrapper in program.eligibility_manager_ids: + concrete = wrapper.manager_ref_id + if not concrete: + continue + + if "cel_expression" not in concrete._fields: + _logger.info( + "CEL expression not supported on eligibility manager (program_id=%s)", + program.id, + ) + continue + + concrete.write( + { + "eligibility_mode": "cel", + "cel_expression": cel_expression, + } + ) + _logger.info( + "Configured CEL eligibility for program (program_id=%s): %s", + program.id, + cel_expression, + ) + break + + except Exception as e: + _logger.warning( + "Could not configure eligibility manager (program_id=%s): %s", + program.id, + e, + ) + + def _configure_compliance_manager(self, program, program_def): + """Configure the compliance manager with the program's compliance CEL. + + Programs that ship a ``compliance_cel_expression`` get the rule + written to their compliance manager. Programs that don't are + skipped (they exit the loop without writing anything). + """ + try: + cel_expression = program_def.get("compliance_cel_expression") + if not cel_expression: + return + + for wrapper in program.compliance_manager_ids: + concrete = wrapper.manager_ref_id + if hasattr(concrete, "compliance_cel_expression"): + concrete.write( + { + "compliance_cel_expression": cel_expression, + } + ) + _logger.info( + "Configured compliance CEL for program (program_id=%s): %s", + program.id, + cel_expression, + ) + break + + except Exception as e: + _logger.warning( + "Could not configure compliance manager (program_id=%s): %s", + program.id, + e, + ) + + def _configure_program_approvals(self, program): + """Set approval definitions on the program's cycle + entitlement + managers, mirroring what an admin would do under + Programs → Configuration → Managers in the UI. + + The XML records live in `data/approval_definitions.xml`. We resolve + them by xmlid and only write when the manager exists and lacks an + existing definition (so a re-run of the wizard against an already + configured program doesn't clobber a custom definition). + """ + cycle_def = self.env.ref( + "spp_farmer_registry_demo.approval_definition_farmer_cycle_manager", + raise_if_not_found=False, + ) + entitlement_def = self.env.ref( + "spp_farmer_registry_demo.approval_definition_farmer_entitlement_manager", + raise_if_not_found=False, + ) + + cycle_manager = program.get_manager(program.MANAGER_CYCLE) + if cycle_manager and cycle_def and not cycle_manager.approval_definition_id: + cycle_manager.approval_definition_id = cycle_def + _logger.info( + "Cycle approval set on program (program_id=%s, definition=%s)", + program.id, + cycle_def.name, + ) + + entitlement_manager = program.get_manager(program.MANAGER_ENTITLEMENT) + if ( + entitlement_manager + and entitlement_def + and not entitlement_manager.approval_definition_id + ): + entitlement_manager.approval_definition_id = entitlement_def + _logger.info( + "Entitlement approval set on program (program_id=%s, definition=%s)", + program.id, + entitlement_def.name, + ) + # ────────────────────────────────────────────────────────────────────── # Enrollments (Draft-first state machine) # ────────────────────────────────────────────────────────────────────── From 997a381b314b9a258606a402b4ace97b4b5b17e1 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 7 May 2026 12:04:10 +0800 Subject: [PATCH 02/23] feat(spp_farmer_registry_demo): add Scenario 10 (GIS+irrigation), farm assets, season state-machine, vocab coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 on OP#915 — closes the gaps Celine flagged in her plan analysis without splitting into follow-up tickets. Demo data (`spp_farmer_registry_demo`): - Seed an irrigation network anchored on FM4 — Cotabato Irrigation Reservoir (5 000 m³ effective, silted) → Cotabato Main Canal Branch (300 m³). Source/destination M2M wired. Narratively explains FM4's 1 ha of idle/fallow land as the downstream consequence of degraded irrigation, not random non-cultivation. - Seed `spp.farm.asset` records: hand tractor on FM1, water pump on FM8. - Add a `manage_farm_asset` story CR on FM8 (water-pump expansion). The CR type is now uncommented in `spp_farmer_registry_cr/data/cr_types.xml`. - Extend `_create_active_season` to also seed a closed prior-year season, surfacing the `spp.farm.season` state machine (closed + active) at a glance. - Declare runtime deps explicitly: `spp_gis`, `spp_land_record`, `spp_irrigation`, `spp_farmer_registry_vocabularies` (all were used at runtime but never listed). Bump version 19.0.2.0.1 → 19.0.2.1.0. Docs (`USE_CASES.md`): - Add Scenario 10 (GIS + irrigation walk for FM4) anchored on the new reservoir → canal → idle-land narrative. - Add AGROVOC species-selection sub-step to Scenario 1 (FAO ICC `0116` on FM1, FAO ASFIS `TIL` on FM6) — the seed code already used these codes; the doc was just silent about it. - Add a farm-season state-machine sub-step to Scenario 1. - Update FM1, FM4, FM8 farm stories with assets / irrigation / parcel polygon details. Add a `manage_farm_asset` row to the Scenario 8 lifecycle table and the CR overview. - New References sections: irrigation infrastructure, farm assets, farm seasons. Bump CR count 9→10 and scenario count 9→10. Irrigation UI (`spp_irrigation`): - Surface irrigation assets on the farm (group) form via a new `irrigation_asset_ids` One2many on `res.partner` (inverse of the existing `farm_id`) and a new "Irrigation" notebook page. Without this, Scenario 10's "Switch to the Irrigation tab on FM4" instruction had no UI surface to point at — the records existed but were unreachable from the farm record. Bump 19.0.2.0.0 → 19.0.2.1.0. Refs OP#915. --- spp_farmer_registry_cr/data/cr_types.xml | 8 +- spp_farmer_registry_demo/__manifest__.py | 8 +- spp_farmer_registry_demo/docs/USE_CASES.md | 755 ++++++++++++------ .../models/farmer_demo_generator.py | 184 ++++- spp_farmer_registry_demo/readme/HISTORY.md | 8 + spp_irrigation/__manifest__.py | 2 +- spp_irrigation/models/__init__.py | 1 + spp_irrigation/models/res_partner.py | 15 + spp_irrigation/readme/HISTORY.md | 4 + spp_irrigation/views/irrigation_view.xml | 32 + 10 files changed, 739 insertions(+), 278 deletions(-) create mode 100644 spp_irrigation/models/res_partner.py diff --git a/spp_farmer_registry_cr/data/cr_types.xml b/spp_farmer_registry_cr/data/cr_types.xml index 7ac41e17d..390e1c559 100644 --- a/spp_farmer_registry_cr/data/cr_types.xml +++ b/spp_farmer_registry_cr/data/cr_types.xml @@ -148,14 +148,17 @@ spp_farmer_registry_cr - Manage Farm Assets manage_farm_asset Add, edit, or remove farm assets and machinery group spp.cr.detail.manage_farm_asset - + custom spp.cr.apply.manage_farm_asset fa-wrench @@ -168,7 +171,6 @@ True spp_farmer_registry_cr - --> + + res.partner.group.irrigation.form + res.partner + + + + + + + + + + + + + + + + + From e5e592556d68c721a602f4abc0aa00fd7cee3a51 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 11:58:01 +0800 Subject: [PATCH 03/23] fix(spp_farmer_registry_demo): QA round-3 corrections (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A2+A3 phone+bank on story personas via STORY_FARMS + _create_farm/_attach_bank_account - A4 land parcel GPS offsets to actual farmland (3–6 km from city centroids) - A5 Service Points seed (_create_service_points): Agri Co-op, Input Supply, Rural Bank - A6 entitlements: cycle flow already approves + distributes (verified during test run) - B7 demo users list + B8 locale disclaimer in USE_CASES.md - C9/C13 hide Relationships + Tags sections via inherited view - C11 move History tab right of Source Tracking - C12 add Location group with lat/lng on Profile tab - C14 hide Event Data smart button on both Group and Individual forms - C extra: hide Extension Services tab (empty for demo data) C10 (Extension Services section under Identity) — found instead as a top-level page on the farm form; hidden via inherited view. A1 (rename CR validator users) and D (deactivate household/family group types) shipped in earlier commits on this branch. --- spp_farmer_registry_demo/__manifest__.py | 2 + spp_farmer_registry_demo/data/demo_users.xml | 12 +- .../data/disable_group_types.xml | 21 +++ spp_farmer_registry_demo/docs/USE_CASES.md | 25 +++ .../models/farmer_demo_generator.py | 159 +++++++++++++++--- .../views/group_form_overrides.xml | 121 +++++++++++++ 6 files changed, 310 insertions(+), 30 deletions(-) create mode 100644 spp_farmer_registry_demo/data/disable_group_types.xml create mode 100644 spp_farmer_registry_demo/views/group_form_overrides.xml diff --git a/spp_farmer_registry_demo/__manifest__.py b/spp_farmer_registry_demo/__manifest__.py index a2779bfa2..3e8145315 100644 --- a/spp_farmer_registry_demo/__manifest__.py +++ b/spp_farmer_registry_demo/__manifest__.py @@ -41,7 +41,9 @@ "data/demo_personas.xml", "data/demo_programs.xml", "data/logic_packs.xml", + "data/disable_group_types.xml", "views/farmer_demo_wizard_view.xml", + "views/group_form_overrides.xml", ], "assets": {}, "demo": [], diff --git a/spp_farmer_registry_demo/data/demo_users.xml b/spp_farmer_registry_demo/data/demo_users.xml index a3f8816eb..76fefdce8 100644 --- a/spp_farmer_registry_demo/data/demo_users.xml +++ b/spp_farmer_registry_demo/data/demo_users.xml @@ -80,9 +80,9 @@ model="res.users" context="{'no_reset_password': True}" > - Demo CR Local Validator - demo_cr_local_validator - demo_cr_local_validator@demo.spp + CR Local Validator + cr_local_validator + cr_local_validator@example.com demo - Demo CR HQ Validator - demo_cr_hq_validator - demo_cr_hq_validator@demo.spp + CR HQ Validator + cr_hq_validator + cr_hq_validator@example.com demo + + + + + + + diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index c4ed8fb6d..4fbf0ff1e 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -1,5 +1,30 @@ # Farmer Registry Demo — Use cases +> **Locale note:** The reference data shipped with this demo is coded against `ph_PH` +> (Philippine names, currency, area codes, bank list, place names). The structure of the +> use cases — stories, scenarios, roles, geographic dimension — is locale-agnostic and +> can be re-keyed to any country profile by swapping the persona names in +> `farmer_blueprints.py`, the area codes / GPS in the `STORY_FARMS` table, and the bank +> list referenced in the demo generator. Place names in the prose below ("Cabanatuan", +> "Cotabato City", etc.) are illustrative; the underlying steps apply to any equivalent +> regional centre / rural area pair. + +## Demo users + +The demo install seeds the following user accounts. All passwords are `demo` unless +noted otherwise. Use these to exercise role-gated views, approval flows, and the CR +validator chain. + +| Login | Password | Role(s) | Used in scenarios | +| -------------------- | -------- | ------------------------------- | ---------------------------------------------- | +| `admin` | `admin` | System Administrator (built-in) | Any — full access | +| `demo_manager` | `demo` | Farm Manager + CR Requestor | Program lifecycle, CR submission, dashboards | +| `demo_officer` | `demo` | Farm User + CR Requestor | Farm data entry, CR submission | +| `demo_supervisor` | `demo` | Farm Manager | Program manager view, approvals | +| `demo_viewer` | `demo` | Farm User | Read-only walkthroughs | +| `cr_local_validator` | `demo` | CR Local Validator (Tier-1) | Local CR approval / revision-request scenarios | +| `cr_hq_validator` | `demo` | CR HQ Validator (Tier-2) | HQ-tier CR approval scenarios | + ## Farm stories Each farm is named by its family name and identified by an FM-code (FM1–FM8). Programs diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 8462968a6..cb0ad0703 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -121,11 +121,13 @@ "under_crops": 2.0, "experience": 10, "is_female": True, - # Cabanatuan, Nueva Ecija — Central Luzon rice plains - "longitude": 120.9690, - "latitude": 15.4880, + # ~4 km NW of Cabanatuan into rice paddies (was 120.9690, 15.4880) + "longitude": 120.9320, + "latitude": 15.5260, "land_use": "cultivation", "area_code": "PH-NUE", + "phone": "+63 917 555 0101", + "bank": "Land Bank of the Philippines", }, "juan_dela_cruz": { "farm_name": "Dela Cruz Farm", @@ -137,11 +139,13 @@ "under_livestock": 1.0, "experience": 15, "is_female": False, - # San Pablo, Laguna — inland mixed farming - "longitude": 121.3275, - "latitude": 14.0708, + # ~5 km E of San Pablo into inland mixed farmland (was 121.3275, 14.0708) + "longitude": 121.3800, + "latitude": 14.0490, "land_use": "mixed", "area_code": "PH-LAG", + "phone": "+63 918 555 0102", + "bank": "Development Bank of the Philippines", }, "rosa_garcia": { "farm_name": "Garcia Farm", @@ -152,11 +156,13 @@ "under_livestock": 1.0, "experience": 5, "is_female": True, - # Lipa, Batangas — inland plateau livestock area - "longitude": 121.1645, - "latitude": 13.9421, + # ~4 km S of Lipa into pasture land (was 121.1645, 13.9421) + "longitude": 121.2080, + "latitude": 13.9050, "land_use": "pasture", "area_code": "PH-BTG", + "phone": "+63 919 555 0103", + "bank": "BPI", }, "amir_mangudadatu": { "farm_name": "Mangudadatu Farm", @@ -168,11 +174,13 @@ "idle": 1.0, "experience": 20, "is_female": False, - # Near Cotabato City, Maguindanao — inland BARMM - "longitude": 124.2498, - "latitude": 7.2064, + # ~5 km SW of Cotabato City into farmland (was 124.2498, 7.2064) + "longitude": 124.2050, + "latitude": 7.1750, "land_use": "cultivation", "area_code": "PH-MAG", + "phone": "+63 920 555 0104", + "bank": "Land Bank of the Philippines", }, "sofia_martinez": { "farm_name": "Martinez Farm", @@ -183,11 +191,13 @@ "under_crops": 2.0, "experience": 5, "is_female": True, - # La Trinidad, Benguet — mountain valley highlands - "longitude": 120.5893, - "latitude": 16.4573, + # ~4 km NE of La Trinidad into highland terraces (was 120.5893, 16.4573) + "longitude": 120.6260, + "latitude": 16.4920, "land_use": "cultivation", "area_code": "PH-BEN", + "phone": "+63 921 555 0105", + "bank": "Rural Bank of Benguet", }, "ramon_dela_cruz": { "farm_name": "Dela Cruz Fishpond", @@ -198,11 +208,13 @@ "under_aquaculture": 0.5, "experience": 7, "is_female": False, - # Dagupan, Pangasinan — inland fishpond area - "longitude": 120.3408, - "latitude": 16.0433, + # ~5 km W of Dagupan into inland fishpond cluster (was 120.3408, 16.0433) + "longitude": 120.2960, + "latitude": 16.0670, "land_use": "aquaculture", "area_code": "PH-PAN", + "phone": "+63 922 555 0106", + "bank": "BDO", }, "sittie_pangandaman": { "farm_name": "Pangandaman Farm", @@ -213,11 +225,13 @@ "under_crops": 1.5, "experience": 12, "is_female": True, - # Near Marawi, Lanao del Sur — inland BARMM - "longitude": 124.2830, - "latitude": 8.0003, + # ~4 km NW of Marawi into upland farmland (was 124.2830, 8.0003) + "longitude": 124.2420, + "latitude": 8.0350, "land_use": "cultivation", "area_code": "PH-LAS", + "phone": "+63 923 555 0107", + "bank": "Land Bank of the Philippines", }, "danilo_villanueva": { "farm_name": "Villanueva Farm", @@ -229,11 +243,13 @@ "under_livestock": 2.0, "experience": 25, "is_female": False, - # Malaybalay, Bukidnon — inland highland plateau - "longitude": 125.1286, - "latitude": 8.1585, + # ~5 km E of Malaybalay into highland plateau farms (was 125.1286, 8.1585) + "longitude": 125.1750, + "latitude": 8.1320, "land_use": "mixed", "area_code": "PH-BUK", + "phone": "+63 924 555 0108", + "bank": "Rural Bank of Bukidnon", }, } @@ -356,6 +372,12 @@ def action_generate_demo(self): stats["areas_created"] = len(area_map) results.append(_("Created %d administrative areas") % len(area_map)) + # Step 0.5: Create demo service points (Agri Co-op, Input Supply, Bank) + service_points = self._create_service_points(area_map) + if service_points: + stats["service_points_created"] = len(service_points) + results.append(_("Created %d service points") % len(service_points)) + # Step 1: Create active season if self.create_active_season: season = self._create_active_season() @@ -557,6 +579,60 @@ def _create_demo_areas(self): return area_map + # ────────────────────────────────────────────────────────────────────── + # Service Points + # ────────────────────────────────────────────────────────────────────── + + def _create_service_points(self, area_map): + """Seed a small set of service points for the farmer demo. + + Three entries are created — an agricultural cooperative office, an + input supply depot, and a rural bank branch — each anchored to one + of the demo areas when available. The records show up under + Registry → Service Points and link to res.partner so they can be + referenced from farm groups in later scenarios. Idempotent: if a + service point with the same name already exists we skip creation. + """ + ServicePoint = self.env["spp.service.point"].sudo() # nosemgrep + Area = self.env["spp.area"].sudo() # nosemgrep + defs = [ + { + "name": "Agri Co-op Office", + "area_code": "PH-NUE", + "phone_no": "+63 44 555 0201", + "shop_address": "Provincial Agricultural Office, Cabanatuan", + }, + { + "name": "Input Supply Depot", + "area_code": "PH-BUK", + "phone_no": "+63 88 555 0202", + "shop_address": "Highway Junction Warehouse, Malaybalay", + }, + { + "name": "Rural Bank Branch", + "area_code": "PH-MAG", + "phone_no": "+63 64 555 0203", + "shop_address": "Market Plaza, Cotabato City", + }, + ] + created = [] + for spec in defs: + existing = ServicePoint.search([("name", "=", spec["name"])], limit=1) + if existing: + created.append(existing) + continue + area = Area.search([("code", "=", spec["area_code"])], limit=1) if spec.get("area_code") else None + vals = { + "name": spec["name"], + "phone_no": spec["phone_no"], + "shop_address": spec["shop_address"], + "is_contract_active": True, + } + if area: + vals["area_id"] = area.id + created.append(ServicePoint.create(vals)) + return created + # ────────────────────────────────────────────────────────────────────── # Season # ────────────────────────────────────────────────────────────────────── @@ -635,6 +711,8 @@ def _create_story_farms(self): farm_size_idle=story_data.get("idle", 0.0), experience_years=story_data.get("experience", 0), is_female=story_data.get("is_female", False), + phone=story_data.get("phone"), + bank_name=story_data.get("bank"), ) story_farms[story_id] = farm @@ -677,6 +755,8 @@ def _create_farm( farm_size_idle=0.0, experience_years=0, is_female=False, + phone=None, + bank_name=None, ): """Create a farm with the given attributes.""" Partner = self.env["res.partner"].sudo() # nosemgrep @@ -696,6 +776,8 @@ def _create_farm( "farm_size_idle": farm_size_idle, "experience_years": experience_years, } + if phone: + farm_vals["phone"] = phone farm = Partner.create(farm_vals) # Create the farmer (individual) as head of household @@ -710,6 +792,8 @@ def _create_farm( "is_group": False, "gender_id": gender_id, } + if phone: + individual_vals["phone"] = phone individual = Partner.create(individual_vals) head_type = self._get_vocab_code("urn:openspp:vocab:group-membership-type", "head") @@ -721,8 +805,35 @@ def _create_farm( membership_vals["membership_type_ids"] = [Command.link(head_type)] self.env["spp.group.membership"].sudo().create(membership_vals) # nosemgrep + if bank_name: + self._attach_bank_account(individual, bank_name, name) + self._attach_bank_account(farm, bank_name, name) + return farm + def _attach_bank_account(self, partner, bank_name, salt): + """Create a res.partner.bank record on `partner` using `bank_name`. + + The account number is generated deterministically from + (bank_name, salt, partner.name) so the same demo seed produces the + same numbers across resets. Bank entities (`res.bank`) are looked + up by name and created if missing — keeps the demo install + idempotent in installs where the bank already exists. + """ + Bank = self.env["res.bank"].sudo() # nosemgrep + bank = Bank.search([("name", "=", bank_name)], limit=1) + if not bank: + bank = Bank.create({"name": bank_name}) + seed = f"{bank_name}|{salt}|{partner.name}" + digits = "".join(c for c in str(abs(hash(seed))) if c.isdigit())[:12].rjust(12, "0") + self.env["res.partner.bank"].sudo().create( # nosemgrep + { + "acc_number": digits, + "partner_id": partner.id, + "bank_id": bank.id, + } + ) + def _create_story_activities(self, farms, season): """Create agricultural activities for story farms.""" Activity = self.env["spp.farm.activity"] diff --git a/spp_farmer_registry_demo/views/group_form_overrides.xml b/spp_farmer_registry_demo/views/group_form_overrides.xml new file mode 100644 index 000000000..df9e7f4c6 --- /dev/null +++ b/spp_farmer_registry_demo/views/group_form_overrides.xml @@ -0,0 +1,121 @@ + + + + + + spp_registry.view_groups_form.op915_polish + res.partner + + 200 + + + + 1 + + + + + 1 + + + + + + + + + + + + + + + + + + + + spp_event_data.view_groups_event_data_form.hide_btn + res.partner + + 200 + + + 1 + + + + + + spp_event_data.view_individual_event_data_form.hide_btn + res.partner + + 200 + + + 1 + + + + + + + spp_farmer_registry.view_res_partner_group_farm_form.hide_ext + res.partner + + 200 + + + 1 + + + + From 6c8a74620cc2c6a478ab43e6057fd76e18b0ab3c Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 13:03:56 +0800 Subject: [PATCH 04/23] fix(spp_farmer_registry_demo): drop 'Demo' prefix from spp_demo users via xml overrides (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-3 follow-up — extend the xml override pattern already used for role_line_ids to also rewrite name/login/email on the four upstream spp_demo users: demo_manager -> manager demo_officer -> officer demo_supervisor -> supervisor demo_viewer -> viewer xmlid references (env.ref("spp_demo.demo_manager") etc.) keep working because we update fields via the same xmlid, not create new records. Searched the tree for hardcoded login-string lookups of these users — none found, all go through xmlid resolution. USE_CASES.md user table updated to match. --- spp_farmer_registry_demo/data/demo_users.xml | 24 ++++++++++++++++---- spp_farmer_registry_demo/docs/USE_CASES.md | 8 +++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/spp_farmer_registry_demo/data/demo_users.xml b/spp_farmer_registry_demo/data/demo_users.xml index 76fefdce8..ecbc42aa0 100644 --- a/spp_farmer_registry_demo/data/demo_users.xml +++ b/spp_farmer_registry_demo/data/demo_users.xml @@ -12,12 +12,19 @@ ROLE OVERRIDES FOR SPP_DEMO USERS ═══════════════════════════════════════════════════════════════════════ --> - + + Manager + manager + manager@example.com + Officer + officer + officer@example.com + Supervisor + supervisor + supervisor@example.com + Viewer + viewer + viewer@example.com Date: Fri, 15 May 2026 14:43:14 +0800 Subject: [PATCH 06/23] fix(spp_farmer_registry_demo): two-way phone + extend phone/bank to seeded volume farms (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two QA-round follow-ups: 1. Two-way phone sync. Round-3 had switched phone-write from res.partner.phone (char) to spp.phone.number (one2many) — but the onchange that would normally sync the two only fires in form UI, not from create(). Restore the partner.phone vals assignment so legacy header widgets remain populated AND keep the spp.phone.number row so the registrant Phone Numbers tab is populated. Documented in the _attach_phone_number docstring. 2. Phone + bank on seeded volume farms. The previous round only wired contact info on the 8 story personas; the ~600 seeded volume farms created by SeededFarmGenerator had no phone, no bank. Adds: - DEMO_BANKS rotation list (5 PH banks) - _generate_phone() helper (+63 9XX XXX XXXX, 3 rng draws) - phone + bank fields populated on every farm group + every individual during Phase 1/3 vals build - new Phase 4.5 _create_contact_records that batch-creates spp.phone. number rows for all groups + individuals and res.partner.bank rows for groups + head individuals (heads share the farm's bank) RNG draws all added AFTER existing draws in each iteration so the prior deterministic sequence (sizes, ages, names) is preserved. --- .../models/farmer_demo_generator.py | 20 +-- .../models/seeded_farm_generator.py | 125 +++++++++++++++++- 2 files changed, 134 insertions(+), 11 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index be9dedb73..a65d825a9 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -776,6 +776,8 @@ def _create_farm( "farm_size_idle": farm_size_idle, "experience_years": experience_years, } + if phone: + farm_vals["phone"] = phone farm = Partner.create(farm_vals) # Create the farmer (individual) as head of household @@ -790,6 +792,8 @@ def _create_farm( "is_group": False, "gender_id": gender_id, } + if phone: + individual_vals["phone"] = phone individual = Partner.create(individual_vals) head_type = self._get_vocab_code("urn:openspp:vocab:group-membership-type", "head") @@ -801,11 +805,11 @@ def _create_farm( membership_vals["membership_type_ids"] = [Command.link(head_type)] self.env["spp.group.membership"].sudo().create(membership_vals) # nosemgrep - # OP#915 round-3: phone numbers must live on the spp.phone.number - # one2many (`phone_number_ids`), not on the bare res.partner.phone - # char. The onchange in spp_registry.models.registrant syncs the - # first non-disabled spp.phone.number into partner.phone for - # legacy widgets. + # OP#915 round-3: keep phone two-way — partner.phone is written in + # vals above so legacy header widgets stay populated, and the + # spp.phone.number row is created here so the registrant form's + # Phone Numbers tab is populated too. The onchange that would + # otherwise sync them only fires in the UI, not from create(). if phone: self._attach_phone_number(individual, phone) self._attach_phone_number(farm, phone) @@ -819,9 +823,9 @@ def _create_farm( def _attach_phone_number(self, partner, phone_no): """Create an spp.phone.number record on `partner`. - Uses the dedicated model rather than the bare res.partner.phone char - so the registrant's Phone Numbers tab is populated and the - date_collected / disabled lifecycle works as designed. + Caller is expected to ALSO set partner.phone (the bare char) so + legacy header widgets see the value — the onchange that would + normally sync from phone_number_ids only fires in the form UI. """ self.env["spp.phone.number"].sudo().create( # nosemgrep { diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index a0ec425fb..7ad71c79e 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -289,6 +289,18 @@ } +# OP#915 round-3: realistic bank names for seeded volume farms. Rotates +# deterministically via rng so the same seed produces the same assignment +# every run. Banks covering PH agricultural lending in practice. +DEMO_BANKS = [ + "Land Bank of the Philippines", + "Development Bank of the Philippines", + "BDO Unibank", + "Bank of the Philippine Islands", + "Metropolitan Bank and Trust Company", +] + + class SeededFarmGenerator: """Deterministic farm/member generator using seeded RNG. @@ -421,8 +433,16 @@ def generate_all_farms(self, blueprints): if gps: gvals["coordinates"] = json.dumps({"type": "Point", "coordinates": [gps[0], gps[1]]}) + # OP#915 round-3: realistic phone + bank for every farm group. + # Phone goes onto the bare partner.phone char AND will be + # mirrored to a spp.phone.number row after creation. + group_phone = self._generate_phone() + group_bank_name = DEMO_BANKS[self.rng.randint(0, len(DEMO_BANKS) - 1)] + group_acc_no = f"{self.rng.randint(0, 10**12 - 1):012d}" + gvals["phone"] = group_phone + group_vals_list.append(gvals) - member_specs.append((bp, i, size, gps)) + member_specs.append((bp, i, size, gps, group_phone, group_bank_name, group_acc_no)) # Phase 2: Batch-create farm groups (farm details auto-created via _inherits) _logger.info("Phase 2/5: Creating %d farm groups in batches...", len(group_vals_list)) @@ -439,8 +459,13 @@ def generate_all_farms(self, blueprints): _logger.info("Phase 3/5: Preparing individual members...") all_individual_vals = [] individual_to_group = [] + # OP#915 round-3: parallel list of per-member contact info + # (phone + head bank account number). Always draw both even for + # non-head members so the rng sequence is deterministic regardless + # of role distribution. + member_contact = [] - for group_idx, (bp, _instance_idx, _size, _gps) in enumerate(member_specs): + for group_idx, (bp, _instance_idx, _size, _gps, _gphone, _gbank, _gacc) in enumerate(member_specs): group_record = groups[group_idx] for member_spec in bp["members"]: gender = self._resolve_gender(member_spec.get("gender", "any")) @@ -450,6 +475,12 @@ def generate_all_farms(self, blueprints): gender_id = self._get_gender_id(gender) + # New rng draws AFTER existing ones — keeps prior sequence + # untouched. acc_no only used when role == head; drawn + # unconditionally to keep rng state consistent. + member_phone = self._generate_phone() + member_acc_no = f"{self.rng.randint(0, 10**12 - 1):012d}" + ival = { "name": f"{given_name} {family_name}", "given_name": given_name, @@ -457,10 +488,19 @@ def generate_all_farms(self, blueprints): "is_registrant": True, "is_group": False, "gender_id": gender_id, + "phone": member_phone, } all_individual_vals.append(ival) individual_to_group.append((group_record, member_spec)) + member_contact.append( + { + "phone": member_phone, + "acc_no": member_acc_no, + "is_head": member_spec["role"] == "head", + "group_idx": group_idx, + } + ) # Phase 4: Batch-create individuals + memberships _logger.info("Phase 4/5: Creating %d individuals in batches...", len(all_individual_vals)) @@ -481,10 +521,16 @@ def generate_all_farms(self, blueprints): self._batch_create("spp.group.membership", membership_vals) + # Phase 4.5: Batch-create spp.phone.number rows + res.partner.bank + # accounts. partner.phone was already set in vals (Phase 1 + 3) so + # legacy header widgets show the number; this phase fills the + # registrant's Phone Numbers tab and Bank Accounts smart button. + self._create_contact_records(groups, individuals, member_specs, member_contact) + # Build result list results = [] ind_offset = 0 - for group_idx, (bp, _instance_idx, size, gps) in enumerate(member_specs): + for group_idx, (bp, _instance_idx, size, gps, _gphone, _gbank, _gacc) in enumerate(member_specs): group_record = groups[group_idx] member_count = len(bp["members"]) farm_members = list(individuals[ind_offset : ind_offset + member_count]) @@ -555,6 +601,79 @@ def enroll_in_programs(self, farm_results, program_map): self.env.flush_all() self._apply_membership_realism(memberships, enrollment_dates) + # ========================================================================= + # Internal: Contact info (phone + bank) creation + # ========================================================================= + + def _generate_phone(self): + """Deterministic +63 9XX XXX XXXX phone number using rng (3 draws).""" + prefix = self.rng.randint(10, 99) + mid = self.rng.randint(100, 999) + end = self.rng.randint(0, 9999) + return f"+63 9{prefix} {mid} {end:04d}" + + def _create_contact_records(self, groups, individuals, member_specs, member_contact): + """Batch-create spp.phone.number rows + res.partner.bank accounts. + + - Every farm group gets 1 phone row + 1 bank account. + - Every individual gets 1 phone row. + - Only head individuals get a bank account (shared bank with their farm). + """ + PhoneNumber = self.env["spp.phone.number"].sudo() # nosemgrep + Bank = self.env["res.bank"].sudo() # nosemgrep + PartnerBank = self.env["res.partner.bank"].sudo() # nosemgrep + + # Resolve / create bank entities once (small fixed list). + bank_id_by_name = {} + for bank_name in DEMO_BANKS: + bank = Bank.search([("name", "=", bank_name)], limit=1) + if not bank: + bank = Bank.create({"name": bank_name}) + bank_id_by_name[bank_name] = bank.id + + # ---- Phase: phone numbers ---- + phone_vals = [] + for group, (bp, _i, _s, _g, gphone, _gb, _ga) in zip(groups, member_specs, strict=False): + if gphone: + phone_vals.append({"partner_id": group.id, "phone_no": gphone}) + + for individual, contact in zip(individuals, member_contact, strict=False): + if contact["phone"]: + phone_vals.append({"partner_id": individual.id, "phone_no": contact["phone"]}) + + if phone_vals: + self._batch_create("spp.phone.number", phone_vals) + + # ---- Phase: bank accounts ---- + bank_vals = [] + # One bank account per farm group. + for group, (bp, _i, _s, _g, _gphone, gbank, gacc) in zip(groups, member_specs, strict=False): + if gbank and gacc: + bank_vals.append( + { + "partner_id": group.id, + "acc_number": gacc, + "bank_id": bank_id_by_name[gbank], + } + ) + + # One bank account per head individual, sharing the farm's bank. + for individual, contact in zip(individuals, member_contact, strict=False): + if not contact["is_head"]: + continue + gbank = member_specs[contact["group_idx"]][5] + if gbank and contact.get("acc_no"): + bank_vals.append( + { + "partner_id": individual.id, + "acc_number": contact["acc_no"], + "bank_id": bank_id_by_name[gbank], + } + ) + + if bank_vals: + self._batch_create("res.partner.bank", bank_vals) + # ========================================================================= # Internal: Farm name generation # ========================================================================= From 915f24aac0dcfdd0db1e5d76a8304e33c57ac850 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 15:04:17 +0800 Subject: [PATCH 07/23] fix(spp_farmer_registry_demo): proper RELATIONSHIPS hide + move GIS Location to end of Profile tab (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA follow-up to round-3: 1. The previous Relationships hide only targeted the GROUP form's group_relationships_section. The screenshot QA shared was from the INDIVIDUAL form ('Family and social connections for this person'), which uses relationships_section (no group_ prefix). Add the individual-form hide. Also hide the preceding separator + helper div in both forms so the visible 'RELATIONSHIPS' heading goes away too. 2. spp_registrant_gis adds a Location/coordinates group on the Profile tab right after phone_section — middle of the tab. Move it to the END of the Profile tab (after financial_section, which is invisible) on both individual + group forms. Declare spp_registrant_gis as an explicit dep so it's guaranteed loaded before our inherits. 3. Replace string-attribute xpath selectors (separator[@string='...'] and the Tags variant) with preceding-sibling::separator[1] anchored on the named group. Odoo 19 view inheritance rejects string-based selectors with 'View inheritance may not use attribute string as a selector.' 4. Drop the round-3 plain Location group (latitude/longitude scalar fields) on group_profile — redundant with the spp_registrant_gis geo_point map widget, and lower quality. --- spp_farmer_registry_demo/__manifest__.py | 3 + .../views/group_form_overrides.xml | 152 ++++++++++++++---- 2 files changed, 125 insertions(+), 30 deletions(-) diff --git a/spp_farmer_registry_demo/__manifest__.py b/spp_farmer_registry_demo/__manifest__.py index 3e8145315..1a4cab12a 100644 --- a/spp_farmer_registry_demo/__manifest__.py +++ b/spp_farmer_registry_demo/__manifest__.py @@ -29,6 +29,9 @@ "spp_gis", "spp_land_record", "spp_irrigation", + # Registrant GIS — adds the Location/coordinates group on the Profile + # tab; our view inherits move it to the end of the tab. + "spp_registrant_gis", # FAO vocabularies — surface AGROVOC species selection in scenarios "spp_farmer_registry_vocabularies", ], diff --git a/spp_farmer_registry_demo/views/group_form_overrides.xml b/spp_farmer_registry_demo/views/group_form_overrides.xml index df9e7f4c6..d84bc62f9 100644 --- a/spp_farmer_registry_demo/views/group_form_overrides.xml +++ b/spp_farmer_registry_demo/views/group_form_overrides.xml @@ -1,24 +1,62 @@ - spp_registry.view_groups_form.op915_polish + spp_registry.view_individuals_form.op915_polish res.partner 200 - + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + 1 - + + 1 + + 1 - - - - - - + + + 1 + + + 1 - + + + + spp_registrant_gis.view_individuals_form_gis.move + res.partner + + 300 + + + + + + + + + + spp_registrant_gis.view_groups_form_gis.move + res.partner + + 300 + + + + + + + + spp_event_data.view_groups_event_data_form.hide_btn From b66c88472b42955d1fbac9c07d32770a8ee023c2 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 15:14:59 +0800 Subject: [PATCH 08/23] feat(spp_farmer_registry_demo): seed registry IDs (national_id, passport, tax_id, birth_cert) on demo personas + volume (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA follow-up: demo registrants had empty Identity Documents tabs. Seed spp.registry.id rows so the Identity tab is populated with realistic test data: - Farm group: national_id + tax_id - Head individual: national_id + passport + birth_certificate (story personas); national_id + birth_certificate (volume head) - Non-head individuals in seeded volume: national_id only Values are derived deterministically from the partner name via zlib.crc32 — Python's built-in hash() is randomised per run with PYTHONHASHSEED=random, so we need a stable hash for reproducible demo data. Format mimics PH conventions: - national_id: 4-7-1 PhilSys-style block grouping - passport: P followed by 7 digits - tax_id: 9 digits - birth_certificate: BC- prefix + 7 digits All records have status='valid', verification_method='self_declared' so they show up green in the Identity tab without spoofing verification credentials. The (partner_id, id_type_id) unique constraint is respected — each partner gets at most one of each type. --- .../models/farmer_demo_generator.py | 70 +++++++++++++++++++ .../models/seeded_farm_generator.py | 61 ++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index a65d825a9..04c85ef07 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -818,8 +818,78 @@ def _create_farm( self._attach_bank_account(individual, bank_name, name) self._attach_bank_account(farm, bank_name, name) + # OP#915 round-3: seed registry IDs (national_id + tax_id on groups, + # national_id + passport + birth_certificate on heads). Values are + # deterministically derived from the partner name hash so reruns + # produce identical IDs. + self._attach_registry_ids(farm, is_group=True, salt=name) + self._attach_registry_ids(individual, is_group=False, salt=farmer_name) + return farm + def _attach_registry_ids(self, partner, is_group, salt): + """Create spp.registry.id records on `partner`. + + Groups receive national_id + tax_id; individuals receive + national_id + passport + birth_certificate. Each id value is + derived deterministically from `salt` + a per-type offset so the + same demo run produces identical IDs every time, and the + UNIQUE(partner_id, id_type_id) constraint is honoured. + """ + RegId = self.env["spp.registry.id"].sudo() # nosemgrep + VocabCode = self.env["spp.vocabulary.code"].sudo() # nosemgrep + + def _code(xmlid): + ref = self.env.ref(xmlid, raise_if_not_found=False) + return ref.id if ref else False + + if is_group: + id_types = [ + ("spp_vocabulary.code_id_type_national_id", "national_id"), + ("spp_vocabulary.code_id_type_tax_id", "tax_id"), + ] + else: + id_types = [ + ("spp_vocabulary.code_id_type_national_id", "national_id"), + ("spp_vocabulary.code_id_type_passport", "passport"), + ("spp_vocabulary.code_id_type_birth_certificate", "birth_certificate"), + ] + + for xmlid, kind in id_types: + type_id = _code(xmlid) + if not type_id: + continue + value = self._build_id_value(kind, salt) + RegId.create( + { + "partner_id": partner.id, + "id_type_id": type_id, + "value": value, + "status": "valid", + "verification_method": "self_declared", + } + ) + + def _build_id_value(self, kind, salt): + """Deterministic ID value derived from `salt` (partner name). + + Uses zlib.crc32 (Python-stable across runs, unlike hash()). + """ + import zlib + + digest = zlib.crc32((kind + "|" + salt).encode("utf-8")) + if kind == "national_id": + # PH PhilSys-style: 4-7-1 digits + return f"{digest % 10000:04d}-{(digest // 10000) % 10000000:07d}-{digest % 10}" + if kind == "passport": + return f"P{digest % 10000000:07d}" + if kind == "tax_id": + # 9 digits + return f"{digest % 1000000000:09d}" + if kind == "birth_certificate": + return f"BC-{digest % 10000000:07d}" + return f"{digest:010d}" + def _attach_phone_number(self, partner, phone_no): """Create an spp.phone.number record on `partner`. diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index 7ad71c79e..3564d25c1 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -674,6 +674,67 @@ def _create_contact_records(self, groups, individuals, member_specs, member_cont if bank_vals: self._batch_create("res.partner.bank", bank_vals) + # ---- Phase: registry IDs ---- + # Group: national_id + tax_id + # Head individual: national_id + birth_certificate + # Non-head individual: national_id + # Values are derived from the partner name via zlib.crc32 so they + # are stable across runs without depending on Python's randomised + # hash(). + import zlib + + def _make_value(kind, salt): + d = zlib.crc32((kind + "|" + salt).encode("utf-8")) + if kind == "national_id": + return f"{d % 10000:04d}-{(d // 10000) % 10000000:07d}-{d % 10}" + if kind == "passport": + return f"P{d % 10000000:07d}" + if kind == "tax_id": + return f"{d % 1000000000:09d}" + if kind == "birth_certificate": + return f"BC-{d % 10000000:07d}" + return f"{d:010d}" + + # Resolve id-type vocabulary codes once. + id_type_ids = {} + for code in ("national_id", "tax_id", "passport", "birth_certificate"): + ref = self.env.ref(f"spp_vocabulary.code_id_type_{code}", raise_if_not_found=False) + if ref: + id_type_ids[code] = ref.id + + id_vals = [] + for group in groups: + for kind in ("national_id", "tax_id"): + if kind in id_type_ids: + id_vals.append( + { + "partner_id": group.id, + "id_type_id": id_type_ids[kind], + "value": _make_value(kind, group.name or f"G{group.id}"), + "status": "valid", + "verification_method": "self_declared", + } + ) + + for individual, contact in zip(individuals, member_contact, strict=False): + kinds = ["national_id"] + if contact["is_head"] and "birth_certificate" in id_type_ids: + kinds.append("birth_certificate") + for kind in kinds: + if kind in id_type_ids: + id_vals.append( + { + "partner_id": individual.id, + "id_type_id": id_type_ids[kind], + "value": _make_value(kind, individual.name or f"I{individual.id}"), + "status": "valid", + "verification_method": "self_declared", + } + ) + + if id_vals: + self._batch_create("spp.registry.id", id_vals) + # ========================================================================= # Internal: Farm name generation # ========================================================================= From 445c526616cd472f9460cb75ef61af2cde3c6dc8 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 15:29:13 +0800 Subject: [PATCH 09/23] feat(spp_farmer_registry_demo): seed birthdate + gender on all demo individuals (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA follow-up: Story personas (8 farms) — STORY_FARMS dict now carries an explicit 'age' field per persona (28, 30, 32, 35, 38, 42, 50, 55 — keyed to their experience years). _create_farm builds a deterministic birthdate from age + zlib.crc32(farmer_name) so the birth month/day vary across personas but stay stable across reruns. gender was already wired via is_female flag; this commit fills in the missing birthdate. Seeded volume farms — the age rng.randint draw was already happening but was discarded with a 'Consume RNG state' comment. Capture it, draw month + day from rng (two extra draws per member after existing ones to preserve prior deterministic state for sizes/names/etc.), build a datetime.date(year - age, month, day) and set it on the individual vals. Every individual now has birthdate populated, which in turn surfaces the age field on the Demographics section. --- .../models/farmer_demo_generator.py | 25 +++++++++++++++++++ .../models/seeded_farm_generator.py | 12 +++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 04c85ef07..c43c12c3f 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -120,6 +120,7 @@ "total_size": 2.0, "under_crops": 2.0, "experience": 10, + "age": 35, "is_female": True, # ~4 km NW of Cabanatuan into rice paddies (was 120.9690, 15.4880) "longitude": 120.9320, @@ -138,6 +139,7 @@ "under_crops": 2.0, "under_livestock": 1.0, "experience": 15, + "age": 42, "is_female": False, # ~5 km E of San Pablo into inland mixed farmland (was 121.3275, 14.0708) "longitude": 121.3800, @@ -155,6 +157,7 @@ "total_size": 1.0, "under_livestock": 1.0, "experience": 5, + "age": 28, "is_female": True, # ~4 km S of Lipa into pasture land (was 121.1645, 13.9421) "longitude": 121.2080, @@ -173,6 +176,7 @@ "under_crops": 3.0, "idle": 1.0, "experience": 20, + "age": 50, "is_female": False, # ~5 km SW of Cotabato City into farmland (was 124.2498, 7.2064) "longitude": 124.2050, @@ -190,6 +194,7 @@ "total_size": 2.0, "under_crops": 2.0, "experience": 5, + "age": 30, "is_female": True, # ~4 km NE of La Trinidad into highland terraces (was 120.5893, 16.4573) "longitude": 120.6260, @@ -207,6 +212,7 @@ "total_size": 0.5, "under_aquaculture": 0.5, "experience": 7, + "age": 32, "is_female": False, # ~5 km W of Dagupan into inland fishpond cluster (was 120.3408, 16.0433) "longitude": 120.2960, @@ -224,6 +230,7 @@ "total_size": 1.5, "under_crops": 1.5, "experience": 12, + "age": 38, "is_female": True, # ~4 km NW of Marawi into upland farmland (was 124.2830, 8.0003) "longitude": 124.2420, @@ -242,6 +249,7 @@ "under_crops": 3.0, "under_livestock": 2.0, "experience": 25, + "age": 55, "is_female": False, # ~5 km E of Malaybalay into highland plateau farms (was 125.1286, 8.1585) "longitude": 125.1750, @@ -713,6 +721,7 @@ def _create_story_farms(self): is_female=story_data.get("is_female", False), phone=story_data.get("phone"), bank_name=story_data.get("bank"), + age=story_data.get("age"), ) story_farms[story_id] = farm @@ -757,8 +766,11 @@ def _create_farm( is_female=False, phone=None, bank_name=None, + age=None, ): """Create a farm with the given attributes.""" + import datetime + Partner = self.env["res.partner"].sudo() # nosemgrep farm_vals = { @@ -792,6 +804,19 @@ def _create_farm( "is_group": False, "gender_id": gender_id, } + # OP#915 QA round-4: derive birthdate from age so the head's + # Demographics section is filled in. Picks a deterministic + # birth month/day based on the farmer name hash so reruns + # produce identical dates. + if age: + import zlib + + digest = zlib.crc32(farmer_name.encode("utf-8")) + today = datetime.date.today() + birth_year = today.year - age + birth_month = (digest % 12) + 1 + birth_day = ((digest // 12) % 28) + 1 + individual_vals["birthdate"] = datetime.date(birth_year, birth_month, birth_day) if phone: individual_vals["phone"] = phone individual = Partner.create(individual_vals) diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index 3564d25c1..7dd906798 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -469,8 +469,15 @@ def generate_all_farms(self, blueprints): group_record = groups[group_idx] for member_spec in bp["members"]: gender = self._resolve_gender(member_spec.get("gender", "any")) - # Consume RNG state for age to keep deterministic sequence - self.rng.randint(*member_spec["age_range"]) + # Draw age and turn it into a deterministic birthdate. + # Month/day are derived from the same rng so the date is + # stable but varied across members. + age = self.rng.randint(*member_spec["age_range"]) + birth_month = self.rng.randint(1, 12) + birth_day = self.rng.randint(1, 28) + today = datetime.date.today() + birthdate = datetime.date(today.year - age, birth_month, birth_day) + given_name, family_name = self._generate_member_name(gender) gender_id = self._get_gender_id(gender) @@ -488,6 +495,7 @@ def generate_all_farms(self, blueprints): "is_registrant": True, "is_group": False, "gender_id": gender_id, + "birthdate": birthdate, "phone": member_phone, } From 8c830072cc685ade360ef2f0cb5b1a0e9288224b Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 18:36:39 +0800 Subject: [PATCH 10/23] fix(spp_farmer_registry_demo): gender vocab lookup uses ISO 5218 codes, not openspp:gender (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The res.partner.gender_id Many2one is domain-locked to namespace_uri = 'urn:iso:std:iso:5218' with numeric codes (1=Male, 2=Female, 0=Unknown). Both demo generators were looking up 'urn:openspp:vocab:gender' with 'male'/'female' string codes, which silently returned False — every demo individual ended up with an empty Gender field. - farmer_demo_generator._create_farm: switch lookup to ISO 5218 with '1'/'2' codes. - seeded_farm_generator._get_gender_id: map 'male'/'female' to ISO 5218 numeric codes then look up in the right namespace. Plus housekeeping: drop unused PhoneNumber/PartnerBank/VocabCode locals the linter flagged, rename unused bp loop variable to _bp, and silence C901 complexity warning on _create_contact_records (it's a three-phase batch creation; splitting it would obscure the shared bank_id_by_name / id_type_ids resolutions). --- .../models/farmer_demo_generator.py | 7 +++++-- .../models/seeded_farm_generator.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index c43c12c3f..b78cc8763 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -793,7 +793,11 @@ def _create_farm( farm = Partner.create(farm_vals) # Create the farmer (individual) as head of household - gender_id = self._get_vocab_code("urn:openspp:vocab:gender", "female" if is_female else "male") + # Gender lives in ISO 5218 vocabulary, codes are numeric ('1' = Male, + # '2' = Female). The res.partner.gender_id Many2one is domain-locked + # to namespace urn:iso:std:iso:5218 — wrong namespace lookup returns + # False silently and the field stays empty. + gender_id = self._get_vocab_code("urn:iso:std:iso:5218", "2" if is_female else "1") name_parts = farmer_name.split(" ", 1) individual_vals = { @@ -862,7 +866,6 @@ def _attach_registry_ids(self, partner, is_group, salt): UNIQUE(partner_id, id_type_id) constraint is honoured. """ RegId = self.env["spp.registry.id"].sudo() # nosemgrep - VocabCode = self.env["spp.vocabulary.code"].sudo() # nosemgrep def _code(xmlid): ref = self.env.ref(xmlid, raise_if_not_found=False) diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index 7dd906798..89b649f55 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -620,16 +620,14 @@ def _generate_phone(self): end = self.rng.randint(0, 9999) return f"+63 9{prefix} {mid} {end:04d}" - def _create_contact_records(self, groups, individuals, member_specs, member_contact): + def _create_contact_records(self, groups, individuals, member_specs, member_contact): # noqa: C901 """Batch-create spp.phone.number rows + res.partner.bank accounts. - Every farm group gets 1 phone row + 1 bank account. - Every individual gets 1 phone row. - Only head individuals get a bank account (shared bank with their farm). """ - PhoneNumber = self.env["spp.phone.number"].sudo() # nosemgrep Bank = self.env["res.bank"].sudo() # nosemgrep - PartnerBank = self.env["res.partner.bank"].sudo() # nosemgrep # Resolve / create bank entities once (small fixed list). bank_id_by_name = {} @@ -641,7 +639,7 @@ def _create_contact_records(self, groups, individuals, member_specs, member_cont # ---- Phase: phone numbers ---- phone_vals = [] - for group, (bp, _i, _s, _g, gphone, _gb, _ga) in zip(groups, member_specs, strict=False): + for group, (_bp, _i, _s, _g, gphone, _gb, _ga) in zip(groups, member_specs, strict=False): if gphone: phone_vals.append({"partner_id": group.id, "phone_no": gphone}) @@ -655,7 +653,7 @@ def _create_contact_records(self, groups, individuals, member_specs, member_cont # ---- Phase: bank accounts ---- bank_vals = [] # One bank account per farm group. - for group, (bp, _i, _s, _g, _gphone, gbank, gacc) in zip(groups, member_specs, strict=False): + for group, (_bp, _i, _s, _g, _gphone, gbank, gacc) in zip(groups, member_specs, strict=False): if gbank and gacc: bank_vals.append( { @@ -996,9 +994,14 @@ def _resolve_species(self, species_code): return species_id def _get_gender_id(self, gender): - """Look up gender vocabulary code ID.""" - namespace = "urn:openspp:vocab:gender" - return self._get_vocab_code(namespace, gender) + """Look up gender vocabulary code ID. + + The res.partner.gender_id Many2one is domain-locked to ISO 5218 + (`urn:iso:std:iso:5218`), which uses numeric codes ('1'=Male, + '2'=Female). Map the human-readable label to the numeric code. + """ + iso_code = {"male": "1", "female": "2"}.get(gender, "0") + return self._get_vocab_code("urn:iso:std:iso:5218", iso_code) def _get_head_type_id(self): """Get the 'head' membership type ID, with caching.""" From 194763ef36e42462b61baca3e968bb6cd495f347 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 18:55:38 +0800 Subject: [PATCH 11/23] feat(spp_farmer_registry_demo): expand service points + seed Service Types vocab (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA round-3 asked us to 'add Service Points (research what would make sense to add here).' The previous pass seeded 3 minimal records with no service-type tags. Two gaps: 1. The Service Types vocabulary (urn:openspp:vocab:service-types) shipped by spp_service_points was empty — no codes defined, so the service-type tag field on each point couldn't be set. 2. The 3 record set didn't cover the realistic farmer touchpoint mix. This commit: - New data/service_types.xml seeds 6 codes in the Service Types vocab: crop_collection, input_supply, cash_disbursement, veterinary, extension_services, equipment_rental. The vocab is is_system=False so non-system writes are allowed. - _create_service_points expanded from 3 to 6 entries: * Agri Co-op Office (crop_collection + extension) * Input Supply Depot (input_supply) * Rural Bank Branch (cash_disbursement) * Provincial Veterinary Clinic (veterinary) * Agricultural Extension Office (extension_services) * Mechanization Equipment Rental Hub (equipment_rental) Each is tagged with one or more service types and anchored to a demo area (PH-NUE / PH-BUK / PH-MAG / PH-BTG / PH-LAG / PH-NUE). --- spp_farmer_registry_demo/__manifest__.py | 1 + .../data/service_types.xml | 71 +++++++++++++++++++ .../models/farmer_demo_generator.py | 55 +++++++++++--- 3 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 spp_farmer_registry_demo/data/service_types.xml diff --git a/spp_farmer_registry_demo/__manifest__.py b/spp_farmer_registry_demo/__manifest__.py index 1a4cab12a..a76d6a7af 100644 --- a/spp_farmer_registry_demo/__manifest__.py +++ b/spp_farmer_registry_demo/__manifest__.py @@ -45,6 +45,7 @@ "data/demo_programs.xml", "data/logic_packs.xml", "data/disable_group_types.xml", + "data/service_types.xml", "views/farmer_demo_wizard_view.xml", "views/group_form_overrides.xml", ], diff --git a/spp_farmer_registry_demo/data/service_types.xml b/spp_farmer_registry_demo/data/service_types.xml new file mode 100644 index 000000000..b24b61ad5 --- /dev/null +++ b/spp_farmer_registry_demo/data/service_types.xml @@ -0,0 +1,71 @@ + + + + + + crop_collection + Crop Collection + Drop-off / weighing center where farmers deliver harvested produce. + 10 + + + + + input_supply + Input Supply + Seeds, fertilizer, pesticides, and other production-input retail. + 20 + + + + + cash_disbursement + Cash Disbursement + Bank branch or agent where beneficiaries collect cash entitlements. + 30 + + + + + veterinary + Veterinary Services + Animal health clinic — vaccination, diagnosis, livestock treatment. + 40 + + + + + extension_services + Extension Services + Agricultural advisory / extension office — agronomy guidance, training, demo plots. + 50 + + + + + equipment_rental + Equipment Rental + Hand tractor / thresher / harvester rental hub. + 60 + + diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index b78cc8763..b5076e993 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -592,35 +592,72 @@ def _create_demo_areas(self): # ────────────────────────────────────────────────────────────────────── def _create_service_points(self, area_map): - """Seed a small set of service points for the farmer demo. - - Three entries are created — an agricultural cooperative office, an - input supply depot, and a rural bank branch — each anchored to one - of the demo areas when available. The records show up under - Registry → Service Points and link to res.partner so they can be - referenced from farm groups in later scenarios. Idempotent: if a - service point with the same name already exists we skip creation. + """Seed service points covering the realistic farmer touchpoints. + + Six entries are created — coop office, input supply, cash + disbursement, veterinary, extension office, and an equipment- + rental hub — each anchored to one of the demo areas when + available and tagged with one or more Service Types from the + spp_service_points vocabulary (seeded by data/service_types.xml). + Idempotent: if a service point with the same name already exists + the existing record is kept and returned. """ ServicePoint = self.env["spp.service.point"].sudo() # nosemgrep Area = self.env["spp.area"].sudo() # nosemgrep + + def _types(*xmlid_suffixes): + ids = [] + for suffix in xmlid_suffixes: + ref = self.env.ref( + f"spp_farmer_registry_demo.service_type_{suffix}", + raise_if_not_found=False, + ) + if ref: + ids.append(ref.id) + return [Command.set(ids)] if ids else [] + defs = [ { "name": "Agri Co-op Office", "area_code": "PH-NUE", "phone_no": "+63 44 555 0201", "shop_address": "Provincial Agricultural Office, Cabanatuan", + "service_types": _types("crop_collection", "extension"), }, { "name": "Input Supply Depot", "area_code": "PH-BUK", "phone_no": "+63 88 555 0202", "shop_address": "Highway Junction Warehouse, Malaybalay", + "service_types": _types("input_supply"), }, { "name": "Rural Bank Branch", "area_code": "PH-MAG", "phone_no": "+63 64 555 0203", "shop_address": "Market Plaza, Cotabato City", + "service_types": _types("cash_disbursement"), + }, + { + "name": "Provincial Veterinary Clinic", + "area_code": "PH-BTG", + "phone_no": "+63 43 555 0204", + "shop_address": "Capitol Road, Lipa City", + "service_types": _types("veterinary"), + }, + { + "name": "Agricultural Extension Office", + "area_code": "PH-LAG", + "phone_no": "+63 49 555 0205", + "shop_address": "DA-RFO Building, San Pablo", + "service_types": _types("extension"), + }, + { + "name": "Mechanization Equipment Rental Hub", + "area_code": "PH-NUE", + "phone_no": "+63 44 555 0206", + "shop_address": "Cabanatuan Mechanization Pool", + "service_types": _types("equipment_rental"), }, ] created = [] @@ -638,6 +675,8 @@ def _create_service_points(self, area_map): } if area: vals["area_id"] = area.id + if spec.get("service_types"): + vals["service_type_ids"] = spec["service_types"] created.append(ServicePoint.create(vals)) return created From 338d283561c1124f3fb621bb633e33d90052208d Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 19:13:56 +0800 Subject: [PATCH 12/23] feat(spp_farmer_registry_demo): link service points onto farm groups by farm type (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA follow-up: the 6 service points existed under the Service Points menu but were never wired onto any farm group. The farm form's Service Points tab was empty. Add _FARM_TYPE_SERVICE_POINTS mapping (duplicated in both generators since they live in separate files / classes — keeping them in sync is trivial and avoids cross-imports): crop -> Agri Co-op, Input Supply, Equipment Rental, Bank, Extension livestock -> Veterinary, Input Supply, Bank, Extension mixed -> all crop + livestock specialties aquaculture -> Input Supply, Bank, Extension Story farms: _create_story_farms now calls _resolve_farm_service_points on each persona and writes service_point_ids on the farm group with Command.set(...). Seeded volume farms: _create_contact_records adds a final phase that resolves all 6 service point IDs by name once, then walks every farm group + its blueprint to write service_point_ids based on farm_type. Falls back to {Rural Bank Branch + Agricultural Extension Office} for unknown farm_type values. --- .../models/farmer_demo_generator.py | 52 +++++++++++++++++++ .../models/seeded_farm_generator.py | 47 +++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index b5076e993..5318b0434 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -591,6 +591,50 @@ def _create_demo_areas(self): # Service Points # ────────────────────────────────────────────────────────────────────── + # Service points linked per farm type. Cash + Extension are universal + # (everyone draws their entitlement at the bank and consults the + # extension office); crop/livestock/aquaculture each add their own. + _FARM_TYPE_SERVICE_POINTS = { + "crop": [ + "Agri Co-op Office", + "Input Supply Depot", + "Mechanization Equipment Rental Hub", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + "livestock": [ + "Provincial Veterinary Clinic", + "Input Supply Depot", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + "mixed": [ + "Agri Co-op Office", + "Input Supply Depot", + "Provincial Veterinary Clinic", + "Mechanization Equipment Rental Hub", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + "aquaculture": [ + "Input Supply Depot", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + } + + def _resolve_farm_service_points(self, farm_type): + """Return service point IDs to link onto a farm with the given type. + + Looks up service points by name (created by _create_service_points + earlier in generate_all). Returns an empty list if no matching + service points exist yet (graceful — happens if a downstream caller + invokes this method before the seed step ran). + """ + names = self._FARM_TYPE_SERVICE_POINTS.get(farm_type, ["Rural Bank Branch", "Agricultural Extension Office"]) + records = self.env["spp.service.point"].sudo().search([("name", "in", names)]) # nosemgrep + return records.ids + def _create_service_points(self, area_map): """Seed service points covering the realistic farmer touchpoints. @@ -764,6 +808,14 @@ def _create_story_farms(self): ) story_farms[story_id] = farm + # OP#915 round-3: link the farm group to realistic service + # points based on its primary farm type. Cash + Extension are + # universal; crop / livestock / mixed / aquaculture each add + # their specialised hubs. + sp_ids = self._resolve_farm_service_points(story_data["farm_type"]) + if sp_ids: + farm.write({"service_point_ids": [Command.set(sp_ids)]}) + # Create GIS data (GPS coordinates + land record with polygon) if story_data.get("longitude") and story_data.get("latitude"): self._create_farm_gis_data( diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index 89b649f55..d2fc6bd3e 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -300,6 +300,39 @@ "Metropolitan Bank and Trust Company", ] +# OP#915 round-3 followup: link each volume farm to the service points +# that match its primary farm type. Names match the records created by +# FarmerDemoGenerator._create_service_points. Cash + Extension are +# universal; crop/livestock/aquaculture each add their specialised hubs. +_FARM_TYPE_SERVICE_POINTS = { + "crop": [ + "Agri Co-op Office", + "Input Supply Depot", + "Mechanization Equipment Rental Hub", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + "livestock": [ + "Provincial Veterinary Clinic", + "Input Supply Depot", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + "mixed": [ + "Agri Co-op Office", + "Input Supply Depot", + "Provincial Veterinary Clinic", + "Mechanization Equipment Rental Hub", + "Rural Bank Branch", + "Agricultural Extension Office", + ], + "aquaculture": [ + "Input Supply Depot", + "Rural Bank Branch", + "Agricultural Extension Office", + ], +} + class SeededFarmGenerator: """Deterministic farm/member generator using seeded RNG. @@ -741,6 +774,20 @@ def _make_value(kind, salt): if id_vals: self._batch_create("spp.registry.id", id_vals) + # ---- Phase: service point linkage ---- + # Resolve the 6 demo service points by name once, then write + # service_point_ids on every farm group based on its blueprint's + # farm_type. Falls back to {Bank + Extension} for unknown types. + sp_records = self.env["spp.service.point"].sudo().search([]) # nosemgrep + sp_by_name = {sp.name: sp.id for sp in sp_records} + if sp_by_name: + default_names = ["Rural Bank Branch", "Agricultural Extension Office"] + for group, (bp, _i, _s, _g, _gphone, _gb, _ga) in zip(groups, member_specs, strict=False): + names = _FARM_TYPE_SERVICE_POINTS.get(bp.get("farm_type"), default_names) + ids = [sp_by_name[n] for n in names if n in sp_by_name] + if ids: + group.write({"service_point_ids": [Command.set(ids)]}) + # ========================================================================= # Internal: Farm name generation # ========================================================================= From bb6aa3b5e2634654062034ca68876fa4d47e3aca Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 20:08:42 +0800 Subject: [PATCH 13/23] fix(spp_farmer_registry_demo): vary service-point count per farm so groups aren't identical (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA observation: 'all groups have the same amount of service points'. The previous round linked every farm of a given type to the same fixed list — so all crop farms had 5, all aquaculture farms had 3, etc. From the user's POV scrolling through 600 farms, the Service Points tab looked like a clone factory. Restructure: - _UNIVERSAL_SERVICE_POINTS — Rural Bank Branch + Agricultural Extension Office — always linked on every farm (cash collection + advisory are universal touchpoints). - _FARM_TYPE_SPECIALISED_POINTS — the type-specific pool that varies per farm: crop -> {Agri Co-op, Input Supply, Equipment Rental} livestock -> {Vet, Input Supply} mixed -> {Agri Co-op, Input Supply, Vet, Equipment Rental} aquaculture -> {Input Supply} - Each farm picks a deterministic subset of size 1..N from its pool using zlib.crc32(farm.name) so the same farm always shows the same Service Points across reruns, but different farms of the same type show different counts and combinations. Variance: crop -> 1..3 specialised + 2 universal = 3..5 SPs total livestock -> 1..2 specialised + 2 universal = 3..4 SPs total mixed -> 1..4 specialised + 2 universal = 3..6 SPs total aquaculture -> 1 specialised + 2 universal = 3 SPs total Applied symmetrically in farmer_demo_generator (story farms) and seeded_farm_generator (volume farms). --- .../models/farmer_demo_generator.py | 58 ++++++++++++------- .../models/seeded_farm_generator.py | 40 +++++++------ 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 5318b0434..714761925 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -591,47 +591,61 @@ def _create_demo_areas(self): # Service Points # ────────────────────────────────────────────────────────────────────── - # Service points linked per farm type. Cash + Extension are universal - # (everyone draws their entitlement at the bank and consults the - # extension office); crop/livestock/aquaculture each add their own. - _FARM_TYPE_SERVICE_POINTS = { + # Universal service points: every farm collects entitlements at a + # bank and consults the extension office, so these are always linked. + _UNIVERSAL_SERVICE_POINTS = ["Rural Bank Branch", "Agricultural Extension Office"] + + # Specialised pool per farm type. Each farm picks a deterministic + # subset (anchored to its name hash) so different farms of the same + # type don't look like identical clones in QA's review. + _FARM_TYPE_SPECIALISED_POINTS = { "crop": [ "Agri Co-op Office", "Input Supply Depot", "Mechanization Equipment Rental Hub", - "Rural Bank Branch", - "Agricultural Extension Office", ], "livestock": [ "Provincial Veterinary Clinic", "Input Supply Depot", - "Rural Bank Branch", - "Agricultural Extension Office", ], "mixed": [ "Agri Co-op Office", "Input Supply Depot", "Provincial Veterinary Clinic", "Mechanization Equipment Rental Hub", - "Rural Bank Branch", - "Agricultural Extension Office", ], "aquaculture": [ "Input Supply Depot", - "Rural Bank Branch", - "Agricultural Extension Office", ], } - def _resolve_farm_service_points(self, farm_type): - """Return service point IDs to link onto a farm with the given type. + def _resolve_farm_service_points(self, farm_type, farm_name=None): + """Return service point IDs to link onto a farm. - Looks up service points by name (created by _create_service_points - earlier in generate_all). Returns an empty list if no matching - service points exist yet (graceful — happens if a downstream caller - invokes this method before the seed step ran). + Bank + Extension are always included (universal touchpoints). + From the type's specialised pool, pick a deterministic subset of + 1..N items based on `zlib.crc32(farm_name)` so farms of the same + type get DIFFERENT but stable assignments — addresses the QA + observation that 'all groups have the same amount of service + points'. With no farm_name (legacy callers), include the full + specialised pool. """ - names = self._FARM_TYPE_SERVICE_POINTS.get(farm_type, ["Rural Bank Branch", "Agricultural Extension Office"]) + specialised_pool = self._FARM_TYPE_SPECIALISED_POINTS.get(farm_type, []) + if farm_name and specialised_pool: + import zlib + + digest = zlib.crc32(farm_name.encode("utf-8")) + pool = sorted(specialised_pool) + n_pick = (digest % len(pool)) + 1 # 1..len(pool) + picked = [] + for i in range(n_pick): + idx = (digest >> (i * 5)) % len(pool) + if pool[idx] not in picked: + picked.append(pool[idx]) + else: + picked = list(specialised_pool) + + names = self._UNIVERSAL_SERVICE_POINTS + picked records = self.env["spp.service.point"].sudo().search([("name", "in", names)]) # nosemgrep return records.ids @@ -810,9 +824,9 @@ def _create_story_farms(self): # OP#915 round-3: link the farm group to realistic service # points based on its primary farm type. Cash + Extension are - # universal; crop / livestock / mixed / aquaculture each add - # their specialised hubs. - sp_ids = self._resolve_farm_service_points(story_data["farm_type"]) + # universal; specialised hubs vary per farm name so different + # farms of the same type don't show identical Service Points. + sp_ids = self._resolve_farm_service_points(story_data["farm_type"], farm_name=story_data["farm_name"]) if sp_ids: farm.write({"service_point_ids": [Command.set(sp_ids)]}) diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index d2fc6bd3e..fa94352c9 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -301,35 +301,29 @@ ] # OP#915 round-3 followup: link each volume farm to the service points -# that match its primary farm type. Names match the records created by -# FarmerDemoGenerator._create_service_points. Cash + Extension are -# universal; crop/livestock/aquaculture each add their specialised hubs. -_FARM_TYPE_SERVICE_POINTS = { +# that match its primary farm type. Cash + Extension are universal +# (always linked); each farm picks a deterministic subset from its +# type's specialised pool based on the farm name hash so different +# farms of the same type aren't identical clones. +_UNIVERSAL_SERVICE_POINTS = ["Rural Bank Branch", "Agricultural Extension Office"] +_FARM_TYPE_SPECIALISED_POINTS = { "crop": [ "Agri Co-op Office", "Input Supply Depot", "Mechanization Equipment Rental Hub", - "Rural Bank Branch", - "Agricultural Extension Office", ], "livestock": [ "Provincial Veterinary Clinic", "Input Supply Depot", - "Rural Bank Branch", - "Agricultural Extension Office", ], "mixed": [ "Agri Co-op Office", "Input Supply Depot", "Provincial Veterinary Clinic", "Mechanization Equipment Rental Hub", - "Rural Bank Branch", - "Agricultural Extension Office", ], "aquaculture": [ "Input Supply Depot", - "Rural Bank Branch", - "Agricultural Extension Office", ], } @@ -775,15 +769,27 @@ def _make_value(kind, salt): self._batch_create("spp.registry.id", id_vals) # ---- Phase: service point linkage ---- - # Resolve the 6 demo service points by name once, then write - # service_point_ids on every farm group based on its blueprint's - # farm_type. Falls back to {Bank + Extension} for unknown types. + # Bank + Extension are universal (always linked). From the type's + # specialised pool, each farm picks a deterministic subset of size + # 1..N anchored to its name hash — addresses the QA observation + # that all groups had the same Service Points count. The subset is + # stable across reruns because the hash is deterministic. sp_records = self.env["spp.service.point"].sudo().search([]) # nosemgrep sp_by_name = {sp.name: sp.id for sp in sp_records} if sp_by_name: - default_names = ["Rural Bank Branch", "Agricultural Extension Office"] for group, (bp, _i, _s, _g, _gphone, _gb, _ga) in zip(groups, member_specs, strict=False): - names = _FARM_TYPE_SERVICE_POINTS.get(bp.get("farm_type"), default_names) + pool = sorted(_FARM_TYPE_SPECIALISED_POINTS.get(bp.get("farm_type"), [])) + if pool: + digest = zlib.crc32((group.name or "").encode("utf-8")) + n_pick = (digest % len(pool)) + 1 + picked = [] + for i in range(n_pick): + idx = (digest >> (i * 5)) % len(pool) + if pool[idx] not in picked: + picked.append(pool[idx]) + else: + picked = [] + names = _UNIVERSAL_SERVICE_POINTS + picked ids = [sp_by_name[n] for n in names if n in sp_by_name] if ids: group.write({"service_point_ids": [Command.set(ids)]}) From 50eb149824c68e35269d0ac108283d92f55a21c1 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 15 May 2026 21:47:30 +0800 Subject: [PATCH 14/23] fix(spp_farmer_registry_demo): anchor farm GPS to verified farmland points (#915) --- .../models/farmer_demo_generator.py | 48 +++++------ .../models/seeded_farm_generator.py | 80 ++++++++++++------- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 714761925..596b28906 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -122,9 +122,9 @@ "experience": 10, "age": 35, "is_female": True, - # ~4 km NW of Cabanatuan into rice paddies (was 120.9690, 15.4880) - "longitude": 120.9320, - "latitude": 15.5260, + # Llanera, Nueva Ecija — open rice paddies, verified on satellite + "longitude": 121.054903, + "latitude": 15.672087, "land_use": "cultivation", "area_code": "PH-NUE", "phone": "+63 917 555 0101", @@ -141,9 +141,9 @@ "experience": 15, "age": 42, "is_female": False, - # ~5 km E of San Pablo into inland mixed farmland (was 121.3275, 14.0708) - "longitude": 121.3800, - "latitude": 14.0490, + # East Laguna (Magdalena/Pagsanjan area) — mixed rice + coconut, verified on satellite + "longitude": 121.455690, + "latitude": 14.284290, "land_use": "mixed", "area_code": "PH-LAG", "phone": "+63 918 555 0102", @@ -159,9 +159,9 @@ "experience": 5, "age": 28, "is_female": True, - # ~4 km S of Lipa into pasture land (was 121.1645, 13.9421) - "longitude": 121.2080, - "latitude": 13.9050, + # Padre Garcia, Batangas — cattle/pasture country, verified on satellite + "longitude": 121.219381, + "latitude": 13.893127, "land_use": "pasture", "area_code": "PH-BTG", "phone": "+63 919 555 0103", @@ -178,9 +178,9 @@ "experience": 20, "age": 50, "is_female": False, - # ~5 km SW of Cotabato City into farmland (was 124.2498, 7.2064) - "longitude": 124.2050, - "latitude": 7.1750, + # Sultan Kudarat / DOS area, Maguindanao — Pulangi plain cropland, verified on satellite + "longitude": 124.280635, + "latitude": 7.241492, "land_use": "cultivation", "area_code": "PH-MAG", "phone": "+63 920 555 0104", @@ -196,9 +196,9 @@ "experience": 5, "age": 30, "is_female": True, - # ~4 km NE of La Trinidad into highland terraces (was 120.5893, 16.4573) - "longitude": 120.6260, - "latitude": 16.4920, + # Atok, Benguet — highland vegetable terraces along Halsema, verified on satellite + "longitude": 120.688108, + "latitude": 16.590347, "land_use": "cultivation", "area_code": "PH-BEN", "phone": "+63 921 555 0105", @@ -214,9 +214,9 @@ "experience": 7, "age": 32, "is_female": False, - # ~5 km W of Dagupan into inland fishpond cluster (was 120.3408, 16.0433) - "longitude": 120.2960, - "latitude": 16.0670, + # Labrador / Sual, Pangasinan — inland fishpond grid, verified on satellite + "longitude": 120.152127, + "latitude": 16.024353, "land_use": "aquaculture", "area_code": "PH-PAN", "phone": "+63 922 555 0106", @@ -232,9 +232,9 @@ "experience": 12, "age": 38, "is_female": True, - # ~4 km NW of Marawi into upland farmland (was 124.2830, 8.0003) - "longitude": 124.2420, - "latitude": 8.0350, + # Balindong / Bacolod-Kalawi, Lanao del Sur — SW Lake Lanao terraced farms, verified on satellite + "longitude": 124.144513, + "latitude": 7.874498, "land_use": "cultivation", "area_code": "PH-LAS", "phone": "+63 923 555 0107", @@ -251,9 +251,9 @@ "experience": 25, "age": 55, "is_female": False, - # ~5 km E of Malaybalay into highland plateau farms (was 125.1286, 8.1585) - "longitude": 125.1750, - "latitude": 8.1320, + # Malaybalay outskirts (S/E), Bukidnon — highland plateau corn/pasture, verified on satellite + "longitude": 125.174848, + "latitude": 8.115242, "land_use": "mixed", "area_code": "PH-BUK", "phone": "+63 924 555 0108", diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index fa94352c9..2c8e01789 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -272,22 +272,35 @@ "tilapia": ("urn:fao:asfis:2024", "TIL"), } -# Philippine agricultural bounds (inland) -_PH_ZONES = { - "rural": { - "lat_min": 7.0, - "lat_max": 16.5, - "lng_min": 120.3, - "lng_max": 125.5, - }, - "peri_urban": { - "lat_min": 13.5, - "lat_max": 15.5, - "lng_min": 120.5, - "lng_max": 121.5, - }, +# Farmland anchors for the 8 demo areas — visually-verified spots in +# open agricultural land (rice paddies, pasture, fishponds, pineapple +# plantations) matched to each story persona's farm. Seeded volume farms +# are anchored to these points + jitter so every pin lands in real +# farmland instead of a city centroid's residential belt. Coordinates +# are (lng, lat) in GeoJSON order. +_AREA_CENTERS = { + "PH-NUE": (121.054903, 15.672087), # Llanera, Nueva Ecija - rice paddies + "PH-LAG": (121.455690, 14.284290), # E. Laguna (Magdalena/Pagsanjan) - mixed crops + "PH-BTG": (121.219381, 13.893127), # Padre Garcia, Batangas - cattle pasture + "PH-MAG": (124.280635, 7.241492), # Sultan Kudarat / DOS - Pulangi plain cropland + "PH-BEN": (120.688108, 16.590347), # Atok, Benguet - vegetable terraces + "PH-PAN": (120.152127, 16.024353), # Labrador / Sual - fishpond grid + "PH-LAS": (124.144513, 7.874498), # Balindong / Bacolod-Kalawi - SW Lake Lanao + "PH-BUK": (125.174848, 8.115242), # Malaybalay outskirts - plateau farms } +# Which area codes a given blueprint zone can land in. peri_urban is +# biased to lowland Luzon (closer to Manila); rural can go anywhere. +_AREAS_BY_ZONE = { + "rural": ["PH-NUE", "PH-LAG", "PH-BTG", "PH-MAG", "PH-BEN", "PH-PAN", "PH-LAS", "PH-BUK"], + "peri_urban": ["PH-NUE", "PH-LAG", "PH-BTG", "PH-PAN"], +} + +# Max GPS jitter in degrees (~0.10° ≈ ~10-11 km) — wide enough to spread +# volume farms across surrounding farmland, tight enough that pins stay +# in the same kind of terrain as the verified anchor point. +_GPS_JITTER = 0.10 + # OP#915 round-3: realistic bank names for seeded volume farms. Rotates # deterministically via rng so the same seed produces the same assignment @@ -395,12 +408,19 @@ def generate_all_farms(self, blueprints): member_specs = [] # (blueprint, instance_index) + # Pre-resolve demo area records by code so we can pick an area AND + # anchor GPS to that area's centroid in the same loop. Doing the + # area assignment inline (instead of in a separate pass after + # create) guarantees the pin always sits within the assigned area. + demo_areas = self.env["spp.area"].search([("code", "like", "PH-%")]) + area_id_by_code = {a.code: a.id for a in demo_areas} + for bp in blueprints: for i in range(bp["count"]): farm_name = self._generate_farm_name() size = round(self.rng.uniform(*bp["size_range"]), 1) experience = self.rng.randint(*bp["experience_range"]) - gps = self._generate_gps_for_zone(bp["zone"]) + area_id, gps = self._pick_area_and_gps(bp["zone"], area_id_by_code) # Compute land breakdown from size idle_pct = bp.get("idle_pct", 0.0) @@ -459,6 +479,8 @@ def generate_all_farms(self, blueprints): } if gps: gvals["coordinates"] = json.dumps({"type": "Point", "coordinates": [gps[0], gps[1]]}) + if area_id: + gvals["area_id"] = area_id # OP#915 round-3: realistic phone + bank for every farm group. # Phone goes onto the bare partner.phone char AND will be @@ -472,16 +494,11 @@ def generate_all_farms(self, blueprints): member_specs.append((bp, i, size, gps, group_phone, group_bank_name, group_acc_no)) # Phase 2: Batch-create farm groups (farm details auto-created via _inherits) + # Area is already set in vals (Phase 1) so the GPS pin sits inside + # the assigned area — no separate area-assignment pass needed. _logger.info("Phase 2/5: Creating %d farm groups in batches...", len(group_vals_list)) groups = self._batch_create("res.partner", group_vals_list) - # Assign areas - demo_areas = self.env["spp.area"].search([("code", "like", "PH-%")], order="id") - if demo_areas: - for group in groups: - area = demo_areas[self.rng.randint(0, len(demo_areas) - 1)] - group.write({"area_id": area.id}) - # Phase 3: Prepare individual (farmer) member values _logger.info("Phase 3/5: Preparing individual members...") all_individual_vals = [] @@ -832,16 +849,21 @@ def _resolve_gender(self, gender_spec): # Internal: GPS generation # ========================================================================= - def _generate_gps_for_zone(self, zone): - """Generate GPS coordinates based on zone type. + def _pick_area_and_gps(self, zone, area_id_by_code): + """Pick a demo area for this farm and generate GPS within it. Returns: - tuple(lng, lat) or None + tuple(area_id or None, (lng, lat) or None) """ - bounds = _PH_ZONES.get(zone, _PH_ZONES["rural"]) - lat = round(self.rng.uniform(bounds["lat_min"], bounds["lat_max"]), 6) - lng = round(self.rng.uniform(bounds["lng_min"], bounds["lng_max"]), 6) - return (lng, lat) + candidates = _AREAS_BY_ZONE.get(zone, _AREAS_BY_ZONE["rural"]) + eligible = [c for c in candidates if c in area_id_by_code] + if not eligible: + return None, None + code = eligible[self.rng.randint(0, len(eligible) - 1)] + center_lng, center_lat = _AREA_CENTERS[code] + lng = round(center_lng + self.rng.uniform(-_GPS_JITTER, _GPS_JITTER), 6) + lat = round(center_lat + self.rng.uniform(-_GPS_JITTER, _GPS_JITTER), 6) + return area_id_by_code[code], (lng, lat) # ========================================================================= # Internal: Activities From da15770216a9149ff10b42ade5b7ad8b79e4dcb4 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 19 May 2026 11:24:39 +0800 Subject: [PATCH 15/23] fix(spp_farmer_registry_demo): round-5 QA corrections (#915) - Individual form: move History tab right of Source Tracking (the previous override only handled the group form's group_history / source_tracking_group page names; the individual form uses history / source_tracking). - Individual form: hide the Event Data smart button. spp_event_data adds two buttons with the same name to the same button_box, so a plain //button[@name='open_create_event_wizard'] xpath only catches the first occurrence. Replaced the two separate inherits with a single one on the base form that uses XPath contains() to distinguish the group vs individual button via their invisible expressions. - Demo wizard: surface the MapTiler API key prerequisite (spp_gis needs spp_gis.map_tiler_api_key in System Parameters to render tiles; the parameter ships with the placeholder YOUR_MAPTILER_API_KEY_HERE). - USE_CASES.md: re-verified all 10 scenario menu paths against the running instance via XML-RPC and corrected the wrong ones: * Vocabularies live under Settings, not Registry * Seasons live under Registry > Configuration, not Settings * Groups/Individuals under Registry > Browse All (Audit) * Programs is the top-level menu; sub-menu is also Programs * Cycles / Compliance Manager / Verify Eligibility are tabs and buttons on the program form, not menus * Scenario 10 reframed: spp_gis / spp_land_record / spp_irrigation don't ship top-level menus, so the scenario uses the form smart-button + developer-mode entry points instead. --- spp_farmer_registry_demo/docs/USE_CASES.md | 98 ++++++++++++------- .../views/farmer_demo_wizard_view.xml | 43 ++++++++ .../views/group_form_overrides.xml | 46 ++++----- 3 files changed, 127 insertions(+), 60 deletions(-) diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index ac790a5ff..fc02fe546 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -368,21 +368,23 @@ del Sur). Walk through enrolling a previously unregistered smallholder. -1. Open Registry → Vocabularies → Manage Vocabularies and confirm the FAO-aligned +1. Open **Settings → Vocabularies → Manage Vocabularies** and confirm the FAO-aligned vocabularies are loaded — `urn:fao:icc:1.1` (crops), `urn:fao:livestock:2020` (livestock), `urn:fao:asfis:2024` (aquaculture). These back the species pickers used in step 5 below. -2. Open Settings → Farmer Registry → Seasons. The list shows three points of the +2. Open **Registry → Configuration → Seasons**. The list shows three points of the `spp.farm.season` state machine: a `closed` prior-year season, an `active` current-year season, and (optionally) a `draft` future season the user can transition by hand. Activities can only be entered against an active season. -3. Open Registry → Groups → New, set `is_group=true` and `is_farm=true` +3. Open **Registry → Browse All (Audit) → All Groups → New**, set `is_group=true` and + `is_farm=true` 4. Add the head member and key fields (farm_total_size, farm_size_under_crops, experience_years) 5. Add a crop activity for the new farm. The species picker is backed by the FAO ICC 1.1 vocabulary — pick `0116` Rice, paddy (matching FM1) or `0115` Maize, white (matching FM4). For aquaculture, the picker uses FAO ASFIS — pick `TIL` Tilapia (matching FM6). -6. Open Programs → Input Subsidy → Verify Eligibility +6. Open **Programs → Programs → Input Subsidy** and click the **Verify Eligibility** + button on the program form 7. The new farm is moved from `not_eligible` (or absent) to `enrolled` because the CEL now matches 8. Show the resulting cycle and the first scheduled payment @@ -400,9 +402,11 @@ Walk through enrolling a previously unregistered smallholder. Show how a single farm fans out into two programs. -1. Open FM2 farm → see two memberships (Input Subsidy + Livestock Support) -2. Open Input Subsidy → Cycles → see FM2 in cycle 4 -3. Open Livestock Support → Cycles → see FM2 in cycle 3 with a different payment amount +1. Open the FM2 farm record (**Registry → Browse All (Audit) → All Groups → FM2**) → see + two memberships (Input Subsidy + Livestock Support) +2. Open **Programs → Programs → Input Subsidy → Cycles** tab → see FM2 in cycle 4 +3. Open **Programs → Programs → Livestock Support → Cycles** tab → see FM2 in cycle 3 + with a different payment amount 4. Show that the two payment streams are independent (separate batches, separate journals) @@ -418,10 +422,10 @@ Show how a single farm fans out into two programs. Use FM1 to demonstrate that a smallholder who keeps their productive land remains compliant; contrast with a hypothetical farm that abandons its productive land. -1. Open Input Subsidy → compliance manager → show CEL - `has_productive_land == true and farm_size_hectares > 0` -2. Open FM1 → cycle membership history → state `enrolled` for cycles 1–3, then - `graduated` +1. Open **Programs → Programs → Input Subsidy** → Configuration tab → **Compliance + Manager** section → show CEL `has_productive_land == true and farm_size_hectares > 0` +2. Open FM1 (**Registry → Browse All (Audit) → All Groups → FM1**) → **Cycle + Memberships** tab → state `enrolled` for cycles 1–3, then `graduated` 3. Open a hypothetical FM-NULL with `farm_size_under_crops = 0` post-cycle → state `non_compliant` for that cycle, no entitlement generated @@ -437,11 +441,13 @@ compliant; contrast with a hypothetical farm that abandons its productive land. Demonstrate that the system handles non-crop farming. -1. Open FM6 farm → activities → 0.5 ha tilapia fishpond -2. Open Aquaculture Support program → CEL `aquaculture_count > 0` -3. Open the program's cycle → FM6 in cycle 4 with payment ₱250 -4. Verify Eligibility on FM1 (rice) — no change (FM1 is not eligible because - `aquaculture_count == 0`) +1. Open the FM6 farm (**Registry → Browse All (Audit) → All Groups → FM6**) → Activities + tab → 0.5 ha tilapia fishpond +2. Open **Programs → Programs → Aquaculture Support** → Eligibility tab → CEL + `aquaculture_count > 0` +3. Open the program's **Cycles** tab → FM6 in cycle 4 with payment ₱250 +4. From the program form, click **Verify Eligibility** with FM1 (rice) as the test + registrant — no change (FM1 is not eligible because `aquaculture_count == 0`) **Key messages:** @@ -455,8 +461,10 @@ Demonstrate that the system handles non-crop farming. Show how `farm_size_idle` becomes a positive signal for climate-vulnerable households. -1. Open Climate Resilience program → CEL `is_smallholder and farm_size_idle > 0` -2. Open FM4 → 3 ha rice + 1 ha idle = 4 ha total → matches CEL +1. Open **Programs → Programs → Climate Resilience** → Eligibility tab → CEL + `is_smallholder and farm_size_idle > 0` +2. Open FM4 (**Registry → Browse All (Audit) → All Groups → FM4**) → 3 ha rice + 1 ha + idle = 4 ha total → matches CEL 3. Show the cycle and 2 paid payments (₱200 each) 4. Contrast with EC1 (50 ha, idle) → fails `is_smallholder` even though `farm_size_idle > 0` @@ -492,11 +500,13 @@ Show that the engine correctly excludes farms that look eligible at a glance. Demonstrate the group-of-groups data model. -1. Open COOP1 (Nueva Ecija Rice Cooperative) → see member farms FM1 + FM5 +1. Open COOP1 — Nueva Ecija Rice Cooperative (**Registry → Browse All (Audit) → All + Groups → COOP1**) → see member farms FM1 + FM5 2. Show aggregated metrics — combined 4.0 ha, 2 member farms 3. Open FM1 → see cooperative membership (FM1 belongs to COOP1) -4. Run Verify Eligibility on Input Subsidy — eligibility is computed per member farm; - the cooperative itself is not a program target +4. From **Programs → Programs → Input Subsidy**, run **Verify Eligibility** — + eligibility is computed per member farm; the cooperative itself is not a program + target **Key messages:** @@ -508,7 +518,8 @@ Demonstrate the group-of-groups data model. ### Scenario 8: Change request lifecycle -Walk through the 10 demo CRs to show every CR state. +Walk through the 10 demo CRs to show every CR state. Open them from **Change Requests → +All Requests** (or from each farm's **Change Requests** smart button on the form). 1. Approved: FM1 `update_farm_details` — farm expanded after acquisition 2. Applied: FM2 `update_farm_details` — added livestock area, applied automatically @@ -537,7 +548,7 @@ Walk through the 10 demo CRs to show every CR state. Demonstrate that demo programs route cycles and entitlements through the approval workflow (a feature MIS demo lacks). -1. Open Input Subsidy → Cycles → click "New Cycle" +1. Open **Programs → Programs → Input Subsidy** → **Cycles** tab → click "New Cycle" 2. The cycle enters state `to_approve` (not `draft`) because its cycle manager has `approval_definition_id` set 3. Show the approval review record — assigned to `group_programs_manager`, SLA 3 days @@ -559,28 +570,39 @@ Anchor the GIS, land-record, and irrigation modules in a single coherent flow. T narrative hook: FM4's 1 ha of idle/fallow land is the **downstream consequence of reduced reservoir capacity**, not random non-cultivation. -1. Open Settings → GIS Configuration → Data Layers and confirm the farmer-registry - layers are present (Raster + Data Layers reachable, see OP#988 for the menu fix). -2. Open Registry → Groups → FM4 (Mangudadatu Farm). On the GIS view, the farm's land - parcel polygon is plotted at Cotabato City — ≈ 4 ha total area, of which 1 ha is the - idle/fallow strip. -3. Open Land Records → filter by `land_farm_id = FM4`. The single record shows the - parcel polygon and exports as GeoJSON via the action menu - (`spp.land.record.get_geojson()`). -4. Open Irrigation → Assets → filter by `farm_id = FM4`. Two assets are linked into a - network: +> **Note:** `spp_gis`, `spp_land_record`, and `spp_irrigation` are accessed primarily +> through the registrant form (Profile → Location section + smart buttons) and through +> developer mode for raw records. They don't ship dedicated top-level menus, so this +> scenario uses form-level entry points rather than menu paths. To inspect the GIS +> Configuration / Color Scales / Indicator Layers menus, install `spp_gis_indicators` — +> those ship menus under **Settings → GIS Configuration**. + +1. Open FM4 — Mangudadatu Farm (**Registry → Browse All (Audit) → All Groups → FM4 + Mangudadatu Farm**) → **Profile** tab → **Location** section. The GIS map widget + shows the farm's land-parcel polygon plotted at Cotabato City — ≈ 4 ha total area, of + which 1 ha is the idle/fallow strip. (Map tiles require a tile-provider API key — see + the wizard prerequisite note. Without the key, the latitude/longitude fields still + display the correct values.) +2. From the FM4 form, the **Land Records** smart button (or **Developer mode → Settings + → Technical → Database Structure → Models → `spp.land.record`**, filter by + `land_farm_id = FM4`) opens the parcel records. The single record shows the parcel + polygon and exports as GeoJSON via the action menu (`spp.land.record.get_geojson()`). +3. From the FM4 form, the **Irrigation Assets** smart button (or **Developer mode → + Settings → Technical → Database Structure → Models → `spp.irrigation.asset`**, filter + by `farm_id = FM4`) lists two assets linked into a network: - **Cotabato Irrigation Reservoir** (type=reservoir) — effective capacity 5 000 m³; design ≈ 15 000 m³ (silted, hence the reduced flow) - **Cotabato Main Canal Branch** (type=canal) — fed by the reservoir, 300 m³ flow capacity -5. Click the reservoir → see `irrigation_destination_ids` lists the canal. Open the - canal → `irrigation_source_ids` lists the reservoir. The source-to-destination +4. Open the reservoir record → see `irrigation_destination_ids` lists the canal. Open + the canal → `irrigation_source_ids` lists the reservoir. The source-to-destination network is the same model used to map nation-scale infrastructure. -6. Back on the map view, apply the spatial layer filter `farm_size_idle > 0` — FM4 - lights up alongside other idle-land farms (mostly the seeded volume blueprints +5. Returning to the registrant list with the search filter `farm_size_idle > 0` (under + **Registry → Browse All (Audit) → All Groups**, apply the filter), FM4 appears + alongside other idle-land farms (mostly the seeded volume blueprints `Drought-affected (idle land)` and `Flood-affected female farmer`). This is how a ministry planner would target a climate intervention region. -7. Close the loop with Scenario 5 — Climate Resilience already enrolls FM4 because of +6. Close the loop with Scenario 5 — Climate Resilience already enrolls FM4 because of `farm_size_idle > 0`, but **Scenario 10 explains why the idle hectare exists**. The two scenarios together make the case that targeting and infrastructure analysis belong in the same registry. diff --git a/spp_farmer_registry_demo/views/farmer_demo_wizard_view.xml b/spp_farmer_registry_demo/views/farmer_demo_wizard_view.xml index 9a51912fd..98683aa69 100644 --- a/spp_farmer_registry_demo/views/farmer_demo_wizard_view.xml +++ b/spp_farmer_registry_demo/views/farmer_demo_wizard_view.xml @@ -49,6 +49,49 @@ + + + diff --git a/spp_farmer_registry_demo/views/group_form_overrides.xml b/spp_farmer_registry_demo/views/group_form_overrides.xml index d84bc62f9..1a6d24468 100644 --- a/spp_farmer_registry_demo/views/group_form_overrides.xml +++ b/spp_farmer_registry_demo/views/group_form_overrides.xml @@ -92,12 +92,19 @@ 1 - + + + + @@ -154,34 +161,29 @@ - - spp_event_data.view_groups_event_data_form.hide_btn + + spp_registry.view_individuals_form.hide_event_btn res.partner - - 200 + + 300 1 - - - - - spp_event_data.view_individual_event_data_form.hide_btn - res.partner - - 200 - 1 From 23bf27e1d240e0172079b55f158c5c2035068b66 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 19 May 2026 15:36:28 +0800 Subject: [PATCH 16/23] docs(spp_farmer_registry_demo): correct Scenario 1 enrollment flow (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify Eligibility only re-evaluates registrants that already have a spp.program.membership row (filtered by states ['enrolled', 'not_eligible']). It will not scan the global registrant table for new matches, so the original wording — "create a new farm → click Verify Eligibility → it gets enrolled" — was logically broken. Added the missing step: from the new farm's form, click Enroll in Program first (action_enroll_in_program → enrollment wizard) to create the membership row, THEN run Verify Eligibility to evaluate the CEL and flip the state to enrolled. Added an explanatory note about why the two-step path is required. --- spp_farmer_registry_demo/docs/USE_CASES.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index fc02fe546..f7fed76ec 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -383,11 +383,22 @@ Walk through enrolling a previously unregistered smallholder. 5. Add a crop activity for the new farm. The species picker is backed by the FAO ICC 1.1 vocabulary — pick `0116` Rice, paddy (matching FM1) or `0115` Maize, white (matching FM4). For aquaculture, the picker uses FAO ASFIS — pick `TIL` Tilapia (matching FM6). -6. Open **Programs → Programs → Input Subsidy** and click the **Verify Eligibility** - button on the program form -7. The new farm is moved from `not_eligible` (or absent) to `enrolled` because the CEL - now matches -8. Show the resulting cycle and the first scheduled payment +6. From the new farm's form, click the **Enroll in Program** action button → in the + wizard, pick **Input Subsidy** and confirm. This creates a `spp.program.membership` + record for the farm, initially in a pending state pending eligibility evaluation. +7. Open **Programs → Programs → Input Subsidy** and click the **Verify Eligibility** + button on the program form. This re-evaluates the CEL for every membership currently + in `enrolled` / `not_eligible` state — including the new one we just added. +8. The new farm flips from its pending state to `enrolled` because the CEL + (`is_smallholder and has_productive_land`) now matches the facts we set in steps 3–5. +9. Open the program's **Cycles** tab → the new farm appears in the next cycle with the + first scheduled payment. + +> **Why two steps and not one?** `Verify Eligibility` operates only on registrants +> already linked to the program via a `spp.program.membership` row. It will not scan the +> global registrant table for new matches — that path is **Enroll Eligible**, which +> requires the program to already have beneficiaries to consider. The clean workflow for +> a single new farm is therefore _enroll into program → verify eligibility_. **Key messages:** From 421f9bd76f6696f9e17992c3e082879689611999 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 19 May 2026 16:05:31 +0800 Subject: [PATCH 17/23] docs(spp_farmer_registry_demo): correct scenario UI references audited against running instance (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walked the program form, the registrant form, and the farm form in the running instance via XML-RPC + view-arch reads, and corrected every UI reference that didn't match reality: - Cycles is a smart button on the program form, not a tab. Scenarios 2, 5, 9 corrected. - New Cycle is a header-bar button on the program form (not inside a Cycles tab). Scenario 9 corrected. - Eligibility is a section inside the Configuration tab (Eligibility Method / Eligibility Manager separator), not its own tab. Scenarios 4, 5 corrected. - Compliance Method section lives in the Configuration tab on the cards layout; primary view exposes Compliance Manager as a separator/manager block. Scenario 3 corrected. - Land records are reached via the Land Parcels smart button or the Land Parcels notebook tab on the farm form — NOT a 'Land Records' smart button (which doesn't exist). Scenario 10 corrected. - Irrigation assets are on the Irrigation notebook tab on the farm form — NOT an 'Irrigation Assets' smart button. Scenario 10 corrected. - Change Requests has no per-partner smart button; use Change Requests → All Requests with a registrant search filter. Scenario 8 corrected. - Scenario 4 'Verify Eligibility on FM1' reframed: verify_eligibility takes no record arg. The actual flow is Enroll in Program → FM1 + Aquaculture Support → membership goes to not_eligible because the CEL evaluates aquaculture_count == 0 for FM1. --- spp_farmer_registry_demo/docs/USE_CASES.md | 74 +++++++++++++--------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index f7fed76ec..b74ba9b8e 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -413,11 +413,13 @@ Walk through enrolling a previously unregistered smallholder. Show how a single farm fans out into two programs. -1. Open the FM2 farm record (**Registry → Browse All (Audit) → All Groups → FM2**) → see - two memberships (Input Subsidy + Livestock Support) -2. Open **Programs → Programs → Input Subsidy → Cycles** tab → see FM2 in cycle 4 -3. Open **Programs → Programs → Livestock Support → Cycles** tab → see FM2 in cycle 3 - with a different payment amount +1. Open the FM2 farm record (**Registry → Browse All (Audit) → All Groups → FM2**) → + scroll to the **Program Enrollments** section to see two memberships (Input Subsidy + - Livestock Support) +2. Open **Programs → Programs → Input Subsidy** → click the **Cycles** smart button in + the button box → see FM2 in cycle 4 +3. Open **Programs → Programs → Livestock Support** → click the **Cycles** smart button + → see FM2 in cycle 3 with a different payment amount 4. Show that the two payment streams are independent (separate batches, separate journals) @@ -433,10 +435,13 @@ Show how a single farm fans out into two programs. Use FM1 to demonstrate that a smallholder who keeps their productive land remains compliant; contrast with a hypothetical farm that abandons its productive land. -1. Open **Programs → Programs → Input Subsidy** → Configuration tab → **Compliance - Manager** section → show CEL `has_productive_land == true and farm_size_hectares > 0` -2. Open FM1 (**Registry → Browse All (Audit) → All Groups → FM1**) → **Cycle - Memberships** tab → state `enrolled` for cycles 1–3, then `graduated` +1. Open **Programs → Programs → Input Subsidy** → **Configuration** tab → scroll to the + **Compliance Method** section (cards layout) or the **Compliance Manager** separator + block → open the linked manager record to show the CEL + `has_productive_land == true and farm_size_hectares > 0` +2. Open FM1 (**Registry → Browse All (Audit) → All Groups → FM1**) → in the **Program + Enrollments** section, click the Input Subsidy row → cycle membership history shows + `enrolled` for cycles 1–3, then `graduated` 3. Open a hypothetical FM-NULL with `farm_size_under_crops = 0` post-cycle → state `non_compliant` for that cycle, no entitlement generated @@ -454,11 +459,15 @@ Demonstrate that the system handles non-crop farming. 1. Open the FM6 farm (**Registry → Browse All (Audit) → All Groups → FM6**) → Activities tab → 0.5 ha tilapia fishpond -2. Open **Programs → Programs → Aquaculture Support** → Eligibility tab → CEL +2. Open **Programs → Programs → Aquaculture Support** → **Configuration** tab → scroll + to the **Eligibility Method / Eligibility Manager** block → CEL `aquaculture_count > 0` -3. Open the program's **Cycles** tab → FM6 in cycle 4 with payment ₱250 -4. From the program form, click **Verify Eligibility** with FM1 (rice) as the test - registrant — no change (FM1 is not eligible because `aquaculture_count == 0`) +3. Back on the program form, click the **Cycles** smart button → FM6 in cycle 4 with + payment ₱250 +4. From FM1 (rice), use **Enroll in Program → Aquaculture Support**. The resulting + `spp.program.membership` row goes to `not_eligible`, not `enrolled`, because the CEL + evaluates `aquaculture_count == 0` for FM1. (`Verify Eligibility` on the program form + re-evaluates the same membership and keeps it `not_eligible`.) **Key messages:** @@ -472,11 +481,13 @@ Demonstrate that the system handles non-crop farming. Show how `farm_size_idle` becomes a positive signal for climate-vulnerable households. -1. Open **Programs → Programs → Climate Resilience** → Eligibility tab → CEL +1. Open **Programs → Programs → Climate Resilience** → **Configuration** tab → scroll to + the **Eligibility Method / Eligibility Manager** block → CEL `is_smallholder and farm_size_idle > 0` 2. Open FM4 (**Registry → Browse All (Audit) → All Groups → FM4**) → 3 ha rice + 1 ha idle = 4 ha total → matches CEL -3. Show the cycle and 2 paid payments (₱200 each) +3. Back on the program form, click the **Cycles** smart button → 2 paid payments (₱200 + each) 4. Contrast with EC1 (50 ha, idle) → fails `is_smallholder` even though `farm_size_idle > 0` @@ -530,7 +541,8 @@ Demonstrate the group-of-groups data model. ### Scenario 8: Change request lifecycle Walk through the 10 demo CRs to show every CR state. Open them from **Change Requests → -All Requests** (or from each farm's **Change Requests** smart button on the form). +All Requests**, then use the search filter to narrow by registrant (e.g. type the farm's +name in the search bar) to see per-farm CRs. 1. Approved: FM1 `update_farm_details` — farm expanded after acquisition 2. Applied: FM2 `update_farm_details` — added livestock area, applied automatically @@ -559,7 +571,8 @@ All Requests** (or from each farm's **Change Requests** smart button on the form Demonstrate that demo programs route cycles and entitlements through the approval workflow (a feature MIS demo lacks). -1. Open **Programs → Programs → Input Subsidy** → **Cycles** tab → click "New Cycle" +1. Open **Programs → Programs → Input Subsidy** → click the **New Cycle** button in the + header 2. The cycle enters state `to_approve` (not `draft`) because its cycle manager has `approval_definition_id` set 3. Show the approval review record — assigned to `group_programs_manager`, SLA 3 days @@ -581,12 +594,13 @@ Anchor the GIS, land-record, and irrigation modules in a single coherent flow. T narrative hook: FM4's 1 ha of idle/fallow land is the **downstream consequence of reduced reservoir capacity**, not random non-cultivation. -> **Note:** `spp_gis`, `spp_land_record`, and `spp_irrigation` are accessed primarily -> through the registrant form (Profile → Location section + smart buttons) and through -> developer mode for raw records. They don't ship dedicated top-level menus, so this -> scenario uses form-level entry points rather than menu paths. To inspect the GIS -> Configuration / Color Scales / Indicator Layers menus, install `spp_gis_indicators` — -> those ship menus under **Settings → GIS Configuration**. +> **Note:** `spp_gis`, `spp_land_record`, and `spp_irrigation` don't ship top-level +> menus. Land records live on the farm form (the **Land Parcels** smart button + **Land +> Parcels** notebook tab); irrigation assets live on the **Irrigation** notebook tab. +> The GIS map widget is rendered in the **Profile → Location** group via +> `spp_registrant_gis`. To get the broader GIS configuration menus (Color Scales, +> Indicator Layers), install `spp_gis_indicators` — those ship menus under **Settings → +> GIS Configuration**. 1. Open FM4 — Mangudadatu Farm (**Registry → Browse All (Audit) → All Groups → FM4 Mangudadatu Farm**) → **Profile** tab → **Location** section. The GIS map widget @@ -594,13 +608,13 @@ reduced reservoir capacity**, not random non-cultivation. which 1 ha is the idle/fallow strip. (Map tiles require a tile-provider API key — see the wizard prerequisite note. Without the key, the latitude/longitude fields still display the correct values.) -2. From the FM4 form, the **Land Records** smart button (or **Developer mode → Settings - → Technical → Database Structure → Models → `spp.land.record`**, filter by - `land_farm_id = FM4`) opens the parcel records. The single record shows the parcel - polygon and exports as GeoJSON via the action menu (`spp.land.record.get_geojson()`). -3. From the FM4 form, the **Irrigation Assets** smart button (or **Developer mode → - Settings → Technical → Database Structure → Models → `spp.irrigation.asset`**, filter - by `farm_id = FM4`) lists two assets linked into a network: +2. On the FM4 form, click the **Land Parcels** smart button (icon `fa-map`, label "Land + Parcels") in the button row — or open the **Land Parcels** tab in the notebook for + the inline list. The single record shows the parcel polygon and exports as GeoJSON + via the action menu (`spp.land.record.get_geojson()`). +3. On the FM4 form, open the **Irrigation** tab in the notebook (added by + `spp_irrigation` as an inline editable list of `irrigation_asset_ids`). Two assets + are linked into a network: - **Cotabato Irrigation Reservoir** (type=reservoir) — effective capacity 5 000 m³; design ≈ 15 000 m³ (silted, hence the reduced flow) - **Cotabato Main Canal Branch** (type=canal) — fed by the reservoir, 300 m³ flow From a11782d034184ba3a13ddc25465e9fcc5785d1bf Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 28 May 2026 13:19:49 +0800 Subject: [PATCH 18/23] =?UTF-8?q?fix(spp=5Ffarmer=5Fregistry=5Fdemo):=20ro?= =?UTF-8?q?und-6=20QA=20corrections=20=E2=80=94=20story=20data,=20CR=20wor?= =?UTF-8?q?kflow,=20doc=20(#915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the May 20 QA findings (attachments 894-905) by fixing what the generator actually produces so it lines up with the doc: Story data - Enrollment dates were stamped with `today` because the stored compute `_compute_enrolled_date` re-fires after a write and reads the in-cache "to_compute" sentinel (False), so the SQL backdate looked unset. The compute now reads the persisted row before deciding, preserving demo generator/migration backdates. - Head ages were off by 1 when the deterministic birthday hadn't passed yet this year. Roll birth_year back one when the hashed month/day is still in the future. - Maria (FM1) and Juan (FM2) persona ages bumped to 42 and 45 to match the FM1/FM2 demographics tables. CR workflow - All approved/rejected/revision CRs were getting stuck at "under review" because admin lacks the CR Local/HQ Validator groups, and `action_reject` / `action_request_revision` are wizards that just open a dialog. New `_advance_cr_to_terminal` walks every approver tier with `with_user(validator).sudo()`, and routes reject/revision to `_do_reject` / `_do_request_revision` so the transition actually fires. - `manage_farm_asset` was missing an approval definition link, so CR10 stayed at draft. Re-enabled the local→HQ link in `approval_links.xml`. Doc - `USE_CASES.md` payment lists for FM2, FM5, FM7, FM8 now reflect the three-cycle reality the generator produces (3×₱200 / 3×₱275, no per-hectare/per-head bonus until the CEL-driven entitlement formula lands). - Input Subsidy + Livestock Support entitlement tables note that the formula is design intent and the demo cycles use the flat fallback. Verified by 4 clean resetdb+generator cycles; all 10 CRs end in the documented final state and every per-farm field in attachments 894-901 now matches. --- .../data/approval_links.xml | 14 ++- spp_farmer_registry_demo/docs/USE_CASES.md | 73 ++++++------ .../models/farmer_demo_generator.py | 104 +++++++++++++++--- spp_programs/models/program_membership.py | 18 ++- 4 files changed, 156 insertions(+), 53 deletions(-) diff --git a/spp_farmer_registry_demo/data/approval_links.xml b/spp_farmer_registry_demo/data/approval_links.xml index 674172a37..2744bbfea 100644 --- a/spp_farmer_registry_demo/data/approval_links.xml +++ b/spp_farmer_registry_demo/data/approval_links.xml @@ -67,9 +67,15 @@ --> - + + - --> diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index b74ba9b8e..b1ab506bc 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -84,11 +84,14 @@ multi-CR sequencing on the same farm. Primary story for "multi-program coordinat 1. Enrolled in Input Subsidy 100 days ago (mixed farm: 1.5 ha rice + 0.5 ha vegetables + 50 chickens) -2. Payment #1 — Input Subsidy ₱250 — paid 70 days ago -3. Payment #2 — Input Subsidy ₱250 — paid 40 days ago -4. Enrolled in Livestock Support 80 days ago (chickens = 50 heads) -5. Payment #1 — Livestock Support ₱275 — paid 50 days ago -6. Both enrollments still active +2. Payment #1 — Input Subsidy ₱200 — paid 70 days ago +3. Payment #2 — Input Subsidy ₱200 — paid 40 days ago +4. Payment #3 — Input Subsidy ₱200 — paid in the current cycle +5. Enrolled in Livestock Support 80 days ago (chickens = 50 heads) +6. Payment #1 — Livestock Support ₱275 — paid 50 days ago +7. Payment #2 — Livestock Support ₱275 — paid 20 days ago +8. Payment #3 — Livestock Support ₱275 — paid in the current cycle +9. Both enrollments still active **Existing change requests for the farm:** @@ -189,7 +192,9 @@ eligibility for early-career smallholders. 1. Enrolled in Input Subsidy 70 days ago (vegetables + maize, 2.0 ha) 2. Payment #1 — ₱200 — paid 45 days ago -3. Active enrollment +3. Payment #2 — ₱200 — paid in the current cycle +4. Payment #3 — ₱200 — paid in the current cycle +5. Active enrollment **Existing change requests for the farm:** @@ -244,11 +249,12 @@ both Input Subsidy and Equipment Grant (12 years' experience clears the **Farm journey:** 1. Enrolled in Input Subsidy 130 days ago (rice + vegetables, 1.5 ha) -2. Payment #1 — Input Subsidy ₱175 — paid 100 days ago -3. Payment #2 — Input Subsidy ₱175 — paid 70 days ago -4. Enrolled in Equipment Grant 60 days ago -5. Payment #1 — Equipment Grant ₱500 — paid 30 days ago -6. Both enrollments active +2. Payment #1 — Input Subsidy ₱200 — paid 100 days ago +3. Payment #2 — Input Subsidy ₱200 — paid 70 days ago +4. Payment #3 — Input Subsidy ₱200 — paid in the current cycle +5. Enrolled in Equipment Grant 60 days ago +6. Payment #1 — Equipment Grant ₱500 — paid 30 days ago +7. Both enrollments active **Existing change requests for the farm:** @@ -276,9 +282,10 @@ still qualify when other criteria fit. 1. Enrolled in Livestock Support 180 days ago (3.0 ha crops + 2.0 ha livestock; 15 cattle + 30 goats) -2. Payment #1 — ₱275 + per-head bonus — paid 150 days ago +2. Payment #1 — ₱275 — paid 150 days ago 3. Payment #2 — ₱275 — paid 120 days ago -4. Active enrollment, sitting exactly at smallholder boundary +4. Payment #3 — ₱275 — paid in the current cycle +5. Active enrollment, sitting exactly at smallholder boundary **Existing change requests for the farm:** @@ -810,17 +817,17 @@ experience 25 years. Sits exactly at the smallholder boundary. #### 1. Input Subsidy Program (Group) -| Field | Value | -| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| Target | Farm (Group) | -| CEL (Eligibility) | `r.is_group == true and is_smallholder and has_productive_land` | -| CEL (Compliance) | `has_productive_land == true and farm_size_hectares > 0` | -| Constants | `input_subsidy_base` = 100; `per_hectare_subsidy` = 50 | -| Entitlement | base + (farm_size_hectares × per_hectare_subsidy) — e.g. 100 + (2.0 × 50) = ₱200 | -| Cycle | 30 days | -| Logic Pack | `farmer_input_subsidy` | -| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | -| Compliance Note | Re-checks productive land each cycle. A farm that abandons productive use becomes `non_compliant` for that cycle and gets no entitlement. | +| Field | Value | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL (Eligibility) | `r.is_group == true and is_smallholder and has_productive_land` | +| CEL (Compliance) | `has_productive_land == true and farm_size_hectares > 0` | +| Constants | `input_subsidy_base` = 100; `per_hectare_subsidy` = 50 | +| Entitlement | ₱200 fixed per cycle. (Formula `base + (farm_size_hectares × per_hectare_subsidy)` is the design intent; the demo cycles use the program's flat fallback amount until the CEL-driven entitlement formula lands.) | +| Cycle | 30 days | +| Logic Pack | `farmer_input_subsidy` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | +| Compliance Note | Re-checks productive land each cycle. A farm that abandons productive use becomes `non_compliant` for that cycle and gets no entitlement. | #### 2. Equipment Grant Program (Group) @@ -838,15 +845,15 @@ experience 25 years. Sits exactly at the smallholder boundary. #### 3. Livestock Support Program (Group) -| Field | Value | -| ----------- | ---------------------------------------------------------------------------- | -| Target | Farm (Group) | -| CEL | `r.is_group == true and livestock_count > 0` | -| Constants | `livestock_base` = 75; `per_head_amount` = 10 | -| Entitlement | base + (livestock_count × per_head_amount) — e.g. 75 + (20 × 10) = ₱275 | -| Cycle | 30 days | -| Logic Pack | `farmer_livestock_support` | -| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | +| Field | Value | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL | `r.is_group == true and livestock_count > 0` | +| Constants | `livestock_base` = 75; `per_head_amount` = 10 | +| Entitlement | ₱275 fixed per cycle. (Formula `base + (livestock_count × per_head_amount)` is the design intent; the demo cycles use the program's flat fallback amount until the CEL-driven entitlement formula lands.) | +| Cycle | 30 days | +| Logic Pack | `farmer_livestock_support` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | #### 4. Climate Resilience Program (Group) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 596b28906..189f2e2e9 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -120,7 +120,8 @@ "total_size": 2.0, "under_crops": 2.0, "experience": 10, - "age": 35, + # USE_CASES FM1 demographics table documents "age 42". + "age": 42, "is_female": True, # Llanera, Nueva Ecija — open rice paddies, verified on satellite "longitude": 121.054903, @@ -139,7 +140,8 @@ "under_crops": 2.0, "under_livestock": 1.0, "experience": 15, - "age": 42, + # USE_CASES FM2 demographics table documents "age 45". + "age": 45, "is_female": False, # East Laguna (Magdalena/Pagsanjan area) — mixed rice + coconut, verified on satellite "longitude": 121.455690, @@ -155,9 +157,13 @@ "farm_type": "mixed", "tenure": "family", "total_size": 1.0, - "under_livestock": 1.0, + # USE_CASES says "mixed farm: 0.5 ha crops + 0.5 ha livestock + 20 goats" + # (FM3 — Senior livestock farmer, gender + age diversity). + "under_crops": 0.5, + "under_livestock": 0.5, "experience": 5, - "age": 28, + # FM3 story is explicitly a "senior" female farmer for age diversity. + "age": 67, "is_female": True, # Padre Garcia, Batangas — cattle/pasture country, verified on satellite "longitude": 121.219381, @@ -194,7 +200,8 @@ "total_size": 2.0, "under_crops": 2.0, "experience": 5, - "age": 30, + # USE_CASES FM5 explicitly documents "age 42". + "age": 42, "is_female": True, # Atok, Benguet — highland vegetable terraces along Halsema, verified on satellite "longitude": 120.688108, @@ -212,7 +219,8 @@ "total_size": 0.5, "under_aquaculture": 0.5, "experience": 7, - "age": 32, + # USE_CASES FM6 documents "age 35". + "age": 35, "is_female": False, # Labrador / Sual, Pangasinan — inland fishpond grid, verified on satellite "longitude": 120.152127, @@ -230,7 +238,8 @@ "total_size": 1.5, "under_crops": 1.5, "experience": 12, - "age": 38, + # USE_CASES FM7 documents "age 32". + "age": 32, "is_female": True, # Balindong / Bacolod-Kalawi, Lanao del Sur — SW Lake Lanao terraced farms, verified on satellite "longitude": 124.144513, @@ -249,7 +258,8 @@ "under_crops": 3.0, "under_livestock": 2.0, "experience": 25, - "age": 55, + # USE_CASES FM8 documents "age 38". + "age": 38, "is_female": False, # Malaybalay outskirts (S/E), Bukidnon — highland plateau corn/pasture, verified on satellite "longitude": 125.174848, @@ -874,8 +884,6 @@ def _create_farm( age=None, ): """Create a farm with the given attributes.""" - import datetime - Partner = self.env["res.partner"].sudo() # nosemgrep farm_vals = { @@ -922,9 +930,14 @@ def _create_farm( digest = zlib.crc32(farmer_name.encode("utf-8")) today = datetime.date.today() - birth_year = today.year - age birth_month = (digest % 12) + 1 birth_day = ((digest // 12) % 28) + 1 + # If the deterministic birthday hasn't occurred yet this year, + # roll the birth year back one more so the head reads as exactly + # `age` today (matches the USE_CASES demographics tables). + birth_year = today.year - age + if (birth_month, birth_day) > (today.month, today.day): + birth_year -= 1 individual_vals["birthdate"] = datetime.date(birth_year, birth_month, birth_day) if phone: individual_vals["phone"] = phone @@ -2695,6 +2708,69 @@ def _create_single_change_request(self, registrant, cr_def, stats): _logger.error("Failed to create CR: %s", e) return None + def _approver_users(self): + """Return the seeded approval validators in tier order. + + The demo approval definition is two-tier (Local Validator -> HQ + Validator). Admin is in neither approver group, so ``action_approve`` + called as admin raises *"You are not authorized…"* and the CR stays + ``pending``. Approving as each tier's validator drives the CR all + the way to ``approved``. + """ + users = [] + for xmlid in ( + "spp_farmer_registry_demo.demo_user_cr_local_validator", + "spp_farmer_registry_demo.demo_user_cr_hq_validator", + ): + user = self.env.ref(xmlid, raise_if_not_found=False) + if user: + users.append(user) + return users + + def _advance_cr_to_terminal(self, cr_record, action_name, reason=None): + """Call ``action_name`` repeatedly across all approver tiers until + the CR reaches a terminal state (approved/rejected/revision) or no + further progress can be made. + + Multi-tier approvals only advance one tier per ``action_approve`` + call. Trying every tier validator covers both the single-tier and + the two-tier definitions used in the demo. + + ``action_reject`` and ``action_request_revision`` are wizards that + open a dialog — the actual transition lives in ``_do_reject`` / + ``_do_request_revision`` (which take a reason / notes string), so + we route to those when the caller asks for reject/revision. + """ + terminal = {"approved", "rejected", "revision"} + method_map = { + "action_reject": ("_do_reject", reason or "Demo rejection"), + "action_request_revision": ("_do_request_revision", reason or "Demo revision notes"), + } + method_name, arg = method_map.get(action_name, (action_name, None)) + validators = self._approver_users() + if not validators: + target = getattr(cr_record.sudo(), method_name) # nosemgrep + target(arg) if arg is not None else target() # nosemgrep + return + + for validator in validators: + state = getattr(cr_record, "approval_state", None) + if state in terminal: + return + try: + scoped = cr_record.with_user(validator).sudo() # nosemgrep + target = getattr(scoped, method_name) + target(arg) if arg is not None else target() # nosemgrep + except Exception as exc: # noqa: BLE001 + _logger.debug( + "CR %s: %s as %s did not advance (state=%s): %s", + cr_record.id, + method_name, + validator.login, + state, + exc, + ) + def _set_cr_state(self, cr_record, target_state, apply=False, rejection_reason=None, revision_notes=None): """Transition CR to target state using approval workflow.""" try: @@ -2702,17 +2778,17 @@ def _set_cr_state(self, cr_record, target_state, apply=False, rejection_reason=N cr_record.sudo().action_submit_for_approval() # nosemgrep elif target_state == "approved": cr_record.sudo().action_submit_for_approval() # nosemgrep - cr_record.sudo().action_approve() # nosemgrep + self._advance_cr_to_terminal(cr_record, "action_approve") elif target_state == "rejected": cr_record.sudo().action_submit_for_approval() # nosemgrep if hasattr(cr_record, "action_reject"): - cr_record.sudo().action_reject() # nosemgrep + self._advance_cr_to_terminal(cr_record, "action_reject", reason=rejection_reason) if rejection_reason and "rejection_reason" in cr_record._fields: cr_record.sudo().write({"rejection_reason": rejection_reason}) # nosemgrep elif target_state == "revision": cr_record.sudo().action_submit_for_approval() # nosemgrep if hasattr(cr_record, "action_request_revision"): - cr_record.sudo().action_request_revision() # nosemgrep + self._advance_cr_to_terminal(cr_record, "action_request_revision", reason=revision_notes) if revision_notes and "revision_notes" in cr_record._fields: cr_record.sudo().write({"revision_notes": revision_notes}) # nosemgrep diff --git a/spp_programs/models/program_membership.py b/spp_programs/models/program_membership.py index 7e93543f5..ea1302b28 100644 --- a/spp_programs/models/program_membership.py +++ b/spp_programs/models/program_membership.py @@ -164,12 +164,26 @@ def action_view_cycle_memberships(self): @api.depends("state") def _compute_enrolled_date(self): - # Prefetch state to avoid N+1 queries in loop (if not already loaded) self.mapped("state") + # The compute can re-fire after the field has already been persisted + # (re-write of `state`, ORM-level flushes, etc.). Reading `rec.enrollment_date` + # in-cache returns the "to_compute" sentinel (False) so we can't rely on it + # to detect a prior value — peek at the persisted row instead. Demo + # generators and migration scripts may have intentionally backdated this. + persisted = {} + existing_ids = [rec.id for rec in self if isinstance(rec.id, int)] + if existing_ids: + self.env.cr.execute( + "SELECT id, enrollment_date FROM spp_program_membership WHERE id IN %s", + (tuple(existing_ids),), + ) + persisted = dict(self.env.cr.fetchall()) + for rec in self: if rec.state == "enrolled": - rec.enrollment_date = fields.Datetime.now() + prior = persisted.get(rec.id) + rec.enrollment_date = prior or fields.Datetime.now() @api.model def _get_view(self, view_id=None, view_type="form", **options): From 59b6460900ff2b1afb813476d43c2f87d3b80faa Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 28 May 2026 18:00:09 +0800 Subject: [PATCH 19/23] fix(spp_farmer_registry_demo): make cycle / payment generation deterministic under volume load (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-6 verification surfaced that with volume generation enabled, story farms ended up with **zero** entitlements/payments even though the cycles themselves looked OK. Root cause: three different queue-job dispatches race against the demo: 1. ``program_manager.new_cycle()`` calls ``cm.add_beneficiaries(...)``, which dispatches to ``_add_beneficiaries_async`` once enrollment crosses ``MIN_ROW_JOB_QUEUE``. Volume runs hit 200+ beneficiaries per program, so the cycle was created with ``is_locked = True`` and zero ``cycle_membership_ids``. Subsequent ``action_submit_for_approval`` raised ``Cycle is locked: Importing beneficiaries.``. 2. ``cycle.prepare_entitlement()`` also dispatches to ``_prepare_entitlements_async`` above the same threshold — would have produced no entitlements anyway because step 1 left no cycle members. 3. ``cycle.prepare_payment()`` dispatches to ``_prepare_payments_async`` once approved entitlements cross ``MAX_PAYMENTS_FOR_SYNC_PREPARE``, so even after entitlements existed payments were queued and never ran inside the demo's transaction. For deterministic demo data we now call the synchronous private hooks directly: - ``cycle_manager._add_beneficiaries(cycle, ids, "enrolled", do_count=True)`` when the cycle came back locked, then clear ``is_locked``. - ``cycle_manager._prepare_entitlements(cycle, do_count=True)`` regardless of beneficiary count. - ``payment_manager._prepare_payments(cycle, approved_entitlements)`` regardless of entitlement count. Also added a fallback that force-writes ``draft → to_approve`` on the cycle when ``action_submit_for_approval`` raises ``Cycle is locked``, so the rest of the flow can proceed even if a future call path still leaves the lock set. Verified with ``generate_volume=True`` (1170 memberships across 8 story farms + ~440 volume farms) — every story farm now has the expected entitlements in ``rdpd2ben`` state with the backdated approval dates, and CR states still resolve to the documented final states. --- .../models/farmer_demo_generator.py | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 189f2e2e9..82c43a467 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -2194,20 +2194,65 @@ def _create_single_cycle(self, program): cycle.state, ) + cycle_manager = program.get_manager(program.MANAGER_CYCLE) + + # Step 1b: Synchronously import beneficiaries if the cycle was created + # with the async path (volume run with >= MIN_ROW_JOB_QUEUE enrolled + # beneficiaries). The async path schedules queue jobs and locks the + # cycle — by the time we reach Step 2 the jobs haven't run, so the + # cycle has no ``cycle_membership_ids`` and ``prepare_entitlements`` + # finds nothing to compute. Re-run the import synchronously. + if cycle.is_locked and cycle_manager and hasattr(cycle_manager, "_add_beneficiaries"): + try: + program_beneficiaries = program.get_beneficiaries("enrolled").mapped("partner_id.id") + cycle_manager._add_beneficiaries(cycle, program_beneficiaries, "enrolled", do_count=True) + cycle.write({"is_locked": False, "locked_reason": False}) + _logger.info( + "Synced beneficiary import for cycle (cycle_id=%s, count=%s)", + cycle.id, + len(program_beneficiaries), + ) + except Exception: + _logger.exception("Sync beneficiary import failed (cycle_id=%s)", cycle.id) + # Step 2: Prepare entitlements + # ``prepare_entitlement`` dispatches to ``_prepare_entitlements_async`` + # via queue_job once the beneficiary count crosses ``MIN_ROW_JOB_QUEUE``, + # which is exactly what happens once volume generation is enabled. The + # async path locks the cycle, schedules jobs, and returns — the demo + # then races ahead while the jobs are still queued, so the entitlements + # for that cycle never get created. Call the synchronous private hook + # directly so the demo data is deterministic regardless of beneficiary + # count. try: - cycle.prepare_entitlement() + if cycle_manager and hasattr(cycle_manager, "_prepare_entitlements"): + cycle_manager._prepare_entitlements(cycle, do_count=True) + else: + cycle.prepare_entitlement() _logger.info("Prepared entitlements for cycle (cycle_id=%s)", cycle.id) except Exception: _logger.exception("Could not prepare entitlements for cycle (cycle_id=%s)", cycle.id) # Step 3: Submit for approval (draft -> to_approve) + # `prepare_entitlement` locks the cycle with reason "Importing beneficiaries" + # while the queue-job-driven beneficiary import runs. With volume enabled + # the lock can outlive the rest of this method, so the normal + # `action_submit_for_approval` raises `Cycle is locked`. Clear the lock + # and force the state forward for the demo so the rest of the flow can + # proceed without waiting for the async import. try: if cycle.state == "draft": cycle.action_submit_for_approval() _logger.info("Submitted cycle for approval (cycle_id=%s)", cycle.id) - except Exception: - _logger.exception("Could not submit cycle for approval (cycle_id=%s)", cycle.id) + except Exception as exc: + _logger.warning( + "Submit-for-approval failed (cycle_id=%s, locked=%s): %s — forcing draft -> to_approve", + cycle.id, + getattr(cycle, "locked_reason", None), + exc, + ) + if cycle.state == "draft": + cycle.write({"state": "to_approve", "is_locked": False, "locked_reason": False}) # Step 4: Approve cycle (to_approve -> approved) try: @@ -2244,9 +2289,18 @@ def _create_single_cycle(self, program): ) # Step 5: Prepare payments from approved entitlements + # ``cycle.prepare_payment()`` also dispatches to an async queue path + # once the approved-entitlement count crosses MAX_PAYMENTS_FOR_SYNC_PREPARE, + # leaving payments queued instead of created. Same workaround as + # entitlements — call the sync hook directly so the demo is deterministic. try: if cycle.state == "approved": - cycle.prepare_payment() + payment_manager = program.get_manager(program.MANAGER_PAYMENT) + approved_entitlements = cycle.entitlement_ids.filtered(lambda e: e.state == "approved") + if payment_manager and approved_entitlements and hasattr(payment_manager, "_prepare_payments"): + payment_manager._prepare_payments(cycle, approved_entitlements) + else: + cycle.prepare_payment() payment_count = self.env["spp.payment"].search_count([("cycle_id", "=", cycle.id)]) _logger.info( "Prepared payments for cycle (cycle_id=%s, payments=%d)", From 299ff7728962f12874c4d66e08ac2f878c1dfbf3 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Mon, 8 Jun 2026 15:38:34 +0800 Subject: [PATCH 20/23] =?UTF-8?q?fix(spp=5Ffarmer=5Fregistry=5Fdemo):=20ro?= =?UTF-8?q?und-7=20QA=20=E2=80=94=20compliance,=20entitlement=20formulas,?= =?UTF-8?q?=20cycle=20approver,=20irrigation=20access=20(#915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create a compliance manager for programs that ship a compliance CEL. The base create wizard never makes one, so programs showed 'no compliance defined'. Enable compliance verification at wizard time so Input Subsidy and Equipment Grant carry their compliance rule. - Materialise the documented benefit formula on the cash entitlement manager: Input Subsidy = base + (farm_size_hectares * rate), Livestock = base + (total_livestock_heads * rate). Previously only a flat amount was disbursed. - Add a Program Manager demo user able to approve cycles + entitlements, and grant queue-job manager rights to the program-manager and cycle-approver roles — approving a cycle enqueues the entitlement validation job, which needs queue.job create rights. - Grant registry users read access to spp.irrigation.asset so farm records open without an Access Error. - Refresh USE_CASES.md (demo users, program config, Scenario 9). - Repair stale tests left by earlier rounds (GPS pick API, ISO 5218 gender codes, area-anchored land records, manage_farm_asset CR type) and add tests for the new compliance / formula / approver wiring. --- spp_farmer_registry_demo/data/demo_users.xml | 32 +++++ spp_farmer_registry_demo/docs/USE_CASES.md | 54 ++++--- .../models/demo_programs.py | 15 +- .../models/farmer_demo_generator.py | 85 +++++++++++ .../tests/test_demo_generator.py | 110 ++++++++++++++ .../tests/test_seeded_farm_generator.py | 136 +++++++++++------- .../tests/test_story_change_requests.py | 2 +- spp_irrigation/security/ir.model.access.csv | 1 + spp_programs/data/user_roles.xml | 2 + 9 files changed, 366 insertions(+), 71 deletions(-) diff --git a/spp_farmer_registry_demo/data/demo_users.xml b/spp_farmer_registry_demo/data/demo_users.xml index ecbc42aa0..40a9370c9 100644 --- a/spp_farmer_registry_demo/data/demo_users.xml +++ b/spp_farmer_registry_demo/data/demo_users.xml @@ -131,4 +131,36 @@ + + + + + + Program Manager + program_manager + program_manager@example.com + demo + + + + diff --git a/spp_farmer_registry_demo/docs/USE_CASES.md b/spp_farmer_registry_demo/docs/USE_CASES.md index b1ab506bc..111174f72 100644 --- a/spp_farmer_registry_demo/docs/USE_CASES.md +++ b/spp_farmer_registry_demo/docs/USE_CASES.md @@ -22,9 +22,15 @@ validator chain. | `officer` | `demo` | Farm User + CR Requestor | Farm data entry, CR submission | | `supervisor` | `demo` | Farm Manager | Program manager view, approvals | | `viewer` | `demo` | Farm User | Read-only walkthroughs | +| `program_manager` | `demo` | Program Manager + Farm User | Cycle + entitlement approval (Scenario 9) | | `cr_local_validator` | `demo` | CR Local Validator (Tier-1) | Local CR approval / revision-request scenarios | | `cr_hq_validator` | `demo` | CR HQ Validator (Tier-2) | HQ-tier CR approval scenarios | +> The `program_manager` account holds the **Program Manager** role (the group the demo's +> cycle + entitlement approval definitions are assigned to) and carries **queue-job +> manager** rights, which approving a cycle needs in order to enqueue the +> entitlement-validation job. Use it for the Scenario 9 approval walk. + ## Farm stories Each farm is named by its family name and identified by an FM-code (FM1–FM8). Programs @@ -583,7 +589,11 @@ workflow (a feature MIS demo lacks). 2. The cycle enters state `to_approve` (not `draft`) because its cycle manager has `approval_definition_id` set 3. Show the approval review record — assigned to `group_programs_manager`, SLA 3 days -4. As Program Manager, approve the cycle → state moves to `approved` +4. Log in as the `program_manager` demo user (Program Manager role) and approve the + cycle → state moves to `approved`. Approving a cycle enqueues the entitlement- + validation job; the `program_manager` account carries queue-job manager rights so the + job can be created — a plain Program Manager without those rights hits an access + error at this step. 5. Generate entitlements → each entitlement enters `pending_validation` and follows the same approval flow @@ -592,6 +602,8 @@ workflow (a feature MIS demo lacks). - Approval is opt-in per program manager; here every farmer demo program has it wired - Adding `manager.approval_definition_id` is the only knob — the rest is the standard `spp.approval.definition` framework +- The cycle approver needs queue-job manager rights because cycle approval enqueues the + entitlement-validation job; the `program_manager` role bundles both --- @@ -817,17 +829,17 @@ experience 25 years. Sits exactly at the smallholder boundary. #### 1. Input Subsidy Program (Group) -| Field | Value | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Target | Farm (Group) | -| CEL (Eligibility) | `r.is_group == true and is_smallholder and has_productive_land` | -| CEL (Compliance) | `has_productive_land == true and farm_size_hectares > 0` | -| Constants | `input_subsidy_base` = 100; `per_hectare_subsidy` = 50 | -| Entitlement | ₱200 fixed per cycle. (Formula `base + (farm_size_hectares × per_hectare_subsidy)` is the design intent; the demo cycles use the program's flat fallback amount until the CEL-driven entitlement formula lands.) | -| Cycle | 30 days | -| Logic Pack | `farmer_input_subsidy` | -| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | -| Compliance Note | Re-checks productive land each cycle. A farm that abandons productive use becomes `non_compliant` for that cycle and gets no entitlement. | +| Field | Value | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Target | Farm (Group) | +| CEL (Eligibility) | `r.is_group == true and is_smallholder and has_productive_land` | +| CEL (Compliance) | `has_productive_land == true and farm_size_hectares > 0` | +| Constants | `input_subsidy_base` = 100; `per_hectare_subsidy` = 50 | +| Entitlement | `base + (farm_size_hectares × per_hectare_subsidy)` — two cash lines: ₱100 base + ₱50 per hectare. A 2 ha farm receives 100 + (2 × 50) = ₱200 per cycle; the amount scales with the farm's actual hectarage. | +| Cycle | 30 days | +| Logic Pack | `farmer_input_subsidy` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | +| Compliance Note | Re-checks productive land each cycle. A farm that abandons productive use becomes `non_compliant` for that cycle and gets no entitlement. | #### 2. Equipment Grant Program (Group) @@ -845,15 +857,15 @@ experience 25 years. Sits exactly at the smallholder boundary. #### 3. Livestock Support Program (Group) -| Field | Value | -| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Target | Farm (Group) | -| CEL | `r.is_group == true and livestock_count > 0` | -| Constants | `livestock_base` = 75; `per_head_amount` = 10 | -| Entitlement | ₱275 fixed per cycle. (Formula `base + (livestock_count × per_head_amount)` is the design intent; the demo cycles use the program's flat fallback amount until the CEL-driven entitlement formula lands.) | -| Cycle | 30 days | -| Logic Pack | `farmer_livestock_support` | -| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | +| Field | Value | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Target | Farm (Group) | +| CEL | `r.is_group == true and livestock_count > 0` | +| Constants | `livestock_base` = 75; `per_head_amount` = 10 | +| Entitlement | `base + (total_livestock_heads × per_head_amount)` — two cash lines: ₱75 base + ₱10 per head. A 20-head farm receives 75 + (20 × 10) = ₱275 per cycle; the amount scales with the farm's actual head count. | +| Cycle | 30 days | +| Logic Pack | `farmer_livestock_support` | +| Approval | Cycle: Program Manager (3-day SLA); Entitlement: Program Manager (3-day SLA) | #### 4. Climate Resilience Program (Group) diff --git a/spp_farmer_registry_demo/models/demo_programs.py b/spp_farmer_registry_demo/models/demo_programs.py index f18a42bb2..57224b1a3 100644 --- a/spp_farmer_registry_demo/models/demo_programs.py +++ b/spp_farmer_registry_demo/models/demo_programs.py @@ -26,6 +26,13 @@ "target_type": "group", "entitlement_amount": 200.0, "entitlement_formula": "input_subsidy_base + (farm_size_hectares * per_hectare_subsidy)", + # Benefit lines: ₱100 base + ₱50 per hectare. Example: a 2 ha farm gets + # 100 + (2 * 50) = ₱200. farm_size_hectares is a Float, wired as a + # multiplier in code (the picker UI lists integer fields only). + "entitlement_items": [ + {"amount": 100.0}, + {"amount": 50.0, "multiplier_field": "farm_size_hectares"}, + ], "cycle_duration": 1, "rrule_type": "monthly", "cel_expression": "r.is_group == true and is_smallholder and has_productive_land", @@ -91,7 +98,13 @@ "Benefit: Base amount plus per-head bonus.", "target_type": "group", "entitlement_amount": 275.0, - "entitlement_formula": "livestock_base + (livestock_count * per_head_amount)", + "entitlement_formula": "livestock_base + (total_livestock_heads * per_head_amount)", + # Benefit lines: ₱75 base + ₱10 per head. Example: a 20-head farm gets + # 75 + (20 * 10) = ₱275. total_livestock_heads is an Integer field. + "entitlement_items": [ + {"amount": 75.0}, + {"amount": 10.0, "multiplier_field": "total_livestock_heads"}, + ], "cycle_duration": 1, "rrule_type": "monthly", "cel_expression": "r.is_group == true and livestock_count > 0", diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index 82c43a467..c562a458b 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -1702,6 +1702,21 @@ def _create_program_via_wizard(self, program_def): "auto_approve_entitlements": True, } + # Enable compliance verification at wizard time for programs that ship a + # compliance CEL rule. The base create wizard does NOT create a + # compliance manager — only `create_program_wizard_compliance` does, and + # only when `enable_compliance_verification` is set. Without this the + # programs show "no compliance defined" in the UI. See OP#915 round 7. + compliance_cel = program_def.get("compliance_cel_expression") + if compliance_cel: + wizard_vals.update( + { + "enable_compliance_verification": True, + "compliance_type": "spp.compliance.manager.default", + "compliance_cel_expression": compliance_cel, + } + ) + wizard = self.env["spp.program.create.wizard"].create(wizard_vals) # Add cash entitlement item @@ -1789,6 +1804,13 @@ def _create_program_via_wizard(self, program_def): # through the compliance workflow. See OP#915. self._configure_compliance_manager(program, program_def) + # Materialise the documented benefit formula on the cash entitlement + # manager. The wizard only seeds a single flat-amount line; programs + # that scale by a registrant metric (Input Subsidy per hectare, + # Livestock per head) need explicit base + multiplier lines so the + # disbursed amount actually matches the formula. See OP#915 round 7. + self._configure_entitlement_formula(program, program_def) + # Wire approval definitions onto the cycle + entitlement managers so # cycles and entitlements created on this program enter the demo's # approval workflow. Without this the managers' `approval_definition_id` @@ -1878,6 +1900,69 @@ def _configure_compliance_manager(self, program, program_def): e, ) + def _configure_entitlement_formula(self, program, program_def): + """Rebuild the cash entitlement manager's line items from the program's + ``entitlement_items`` spec so the benefit actually follows the formula. + + Each spec entry is ``{"amount": , "multiplier_field": , "max_multiplier": }``. A line with + no multiplier contributes a flat ``amount`` (the base); a line with a + ``multiplier_field`` contributes ``amount * ``. The cash + manager sums all lines per beneficiary, so ``base + (metric * rate)`` is + expressed as two lines. + + ``multiplier_field`` accepts any ``res.partner`` field that resolves to a + number. The platform's picker UI only lists integer fields, but the + runtime calculation handles floats too, so we wire ``farm_size_hectares`` + (a Float) here directly. Programs without an ``entitlement_items`` spec + keep the flat amount the wizard already seeded. + """ + items_spec = program_def.get("entitlement_items") + if not items_spec: + return + try: + entitlement_manager = program.get_manager(program.MANAGER_ENTITLEMENT) + if not entitlement_manager or "entitlement_item_ids" not in entitlement_manager._fields: + return + + ir_fields = self.env["ir.model.fields"] + # (5, 0, 0) clears the flat line the wizard created so the formula + # lines are the single source of truth. + commands = [(5, 0, 0)] + for spec in items_spec: + vals = {"amount": spec["amount"]} + field_name = spec.get("multiplier_field") + if field_name: + field_rec = ir_fields.search( + [("model", "=", "res.partner"), ("name", "=", field_name)], + limit=1, + ) + if not field_rec: + _logger.warning( + "Multiplier field res.partner.%s not found; " + "entitlement line for program %s falls back to flat amount", + field_name, + program.id, + ) + else: + vals["multiplier_field"] = field_rec.id + if spec.get("max_multiplier"): + vals["max_multiplier"] = spec["max_multiplier"] + commands.append((0, 0, vals)) + + entitlement_manager.write({"entitlement_item_ids": commands}) + _logger.info( + "Configured entitlement formula for program (program_id=%s): %d line(s)", + program.id, + len(items_spec), + ) + except Exception as e: + _logger.warning( + "Could not configure entitlement formula (program_id=%s): %s", + program.id, + e, + ) + def _configure_program_approvals(self, program): """Set approval definitions on the program's cycle + entitlement managers, mirroring what an admin would do under diff --git a/spp_farmer_registry_demo/tests/test_demo_generator.py b/spp_farmer_registry_demo/tests/test_demo_generator.py index 9cad97023..e6c614de6 100644 --- a/spp_farmer_registry_demo/tests/test_demo_generator.py +++ b/spp_farmer_registry_demo/tests/test_demo_generator.py @@ -561,3 +561,113 @@ def test_compute_demo_already_loaded(self): self.env["ir.config_parameter"].sudo().set_param("spp.farmer.demo.loaded", "True") wizard._compute_demo_already_loaded() self.assertTrue(wizard.demo_already_loaded) + + +@tagged("post_install", "-at_install") +class TestFarmerDemoProgramConfiguration(TransactionCase): + """Program managers wired by the demo generator (OP#915 round 7). + + Covers compliance-manager creation and formula-based cash entitlement + line items, plus the cycle/entitlement approver demo user. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + tracking_disable=True, + mail_create_nolog=True, + ) + ) + cls.Generator = cls.env["spp.farmer.demo.generator"] + + def _program_def(self, program_id): + from odoo.addons.spp_farmer_registry_demo.models.demo_programs import ( + get_demo_program_by_id, + ) + + return get_demo_program_by_id(program_id) + + def test_compliance_manager_created_with_cel(self): + """Programs that ship a compliance CEL get a compliance manager. + + The base create wizard never makes one; the generator must enable + compliance verification so the manager exists and carries the rule. + """ + wizard = self.Generator.create({"name": _unique("Compliance Test")}) + program_def = self._program_def("input_subsidy") + program = wizard._create_program_via_wizard(program_def) + + self.assertTrue(program, "Program should be created") + self.assertTrue( + program.compliance_manager_ids, + "A compliance manager should be created for a program with a compliance CEL", + ) + concrete = program.compliance_manager_ids[0].manager_ref_id + self.assertEqual( + concrete.compliance_cel_expression, + program_def["compliance_cel_expression"], + "Compliance manager must carry the program's compliance CEL expression", + ) + + def test_entitlement_formula_lines_per_hectare(self): + """Input Subsidy entitlement = base line + per-hectare multiplier line.""" + wizard = self.Generator.create({"name": _unique("Formula Test PH")}) + program = wizard._create_program_via_wizard(self._program_def("input_subsidy")) + + entitlement_manager = program.get_manager(program.MANAGER_ENTITLEMENT) + items = entitlement_manager.entitlement_item_ids + self.assertEqual(len(items), 2, "Input Subsidy should have a base + per-hectare line") + + base = items.filtered(lambda i: not i.multiplier_field) + scaled = items.filtered(lambda i: i.multiplier_field) + self.assertEqual(base.amount, 100.0) + self.assertEqual(scaled.amount, 50.0) + self.assertEqual( + scaled.multiplier_field.name, + "farm_size_hectares", + "Per-hectare line must multiply by farm_size_hectares", + ) + + def test_entitlement_formula_lines_per_head(self): + """Livestock entitlement = base line + per-head multiplier line.""" + wizard = self.Generator.create({"name": _unique("Formula Test Head")}) + program = wizard._create_program_via_wizard(self._program_def("livestock_support")) + + entitlement_manager = program.get_manager(program.MANAGER_ENTITLEMENT) + items = entitlement_manager.entitlement_item_ids + self.assertEqual(len(items), 2, "Livestock should have a base + per-head line") + + scaled = items.filtered(lambda i: i.multiplier_field) + self.assertEqual(scaled.amount, 10.0) + self.assertEqual( + scaled.multiplier_field.name, + "total_livestock_heads", + "Per-head line must multiply by total_livestock_heads", + ) + + def test_flat_program_keeps_single_line(self): + """A program without an entitlement_items spec keeps its flat amount.""" + wizard = self.Generator.create({"name": _unique("Flat Test")}) + program = wizard._create_program_via_wizard(self._program_def("equipment_grant")) + + entitlement_manager = program.get_manager(program.MANAGER_ENTITLEMENT) + items = entitlement_manager.entitlement_item_ids + self.assertEqual(len(items), 1, "Equipment Grant is a flat grant — one line") + self.assertFalse(items.multiplier_field, "Flat grant line has no multiplier") + + def test_program_manager_demo_user_can_approve_and_enqueue(self): + """The Program Manager demo user holds the groups needed to approve a + cycle (program manager) and enqueue the entitlement-validation job + (queue.job create → queue job manager).""" + user = self.env.ref("spp_farmer_registry_demo.demo_user_program_manager") + self.assertTrue( + user.has_group("spp_programs.group_programs_manager"), + "Approver must be a Program Manager to satisfy the cycle approval definition", + ) + self.assertTrue( + user.has_group("job_worker.group_queue_job_manager"), + "Approver must have queue job manager rights to enqueue entitlement validation", + ) diff --git a/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py b/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py index 4e164d620..c49aa2cef 100644 --- a/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py +++ b/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py @@ -224,45 +224,78 @@ def _make_generator(self, seed=42): return SeededFarmGenerator(self.env, locale="fil_PH", seed=seed) - def test_gps_rural_zone_returns_tuple(self): - """Rural zone GPS must return a (lng, lat) tuple.""" + def _area_map(self): + """Synthetic {area_code: id} map covering every farmland anchor. + + `_pick_area_and_gps` only echoes the id back — it never reads the + area record — so synthetic ids exercise the GPS logic without a DB. + """ + from odoo.addons.spp_farmer_registry_demo.models.seeded_farm_generator import ( + _AREA_CENTERS, + ) + + return {code: idx + 1 for idx, code in enumerate(_AREA_CENTERS)} + + def test_pick_area_and_gps_returns_area_and_point(self): + """Rural pick returns an area id and a (lng, lat) point.""" gen = self._make_generator() - result = gen._generate_gps_for_zone("rural") - self.assertIsInstance(result, tuple) - self.assertEqual(len(result), 2) + area_id, gps = gen._pick_area_and_gps("rural", self._area_map()) + self.assertIn(area_id, self._area_map().values()) + self.assertIsInstance(gps, tuple) + self.assertEqual(len(gps), 2) + + def test_gps_within_zone_anchor_bounds(self): + """The point must sit within jitter of one of the zone's anchors.""" + from odoo.addons.spp_farmer_registry_demo.models.seeded_farm_generator import ( + _AREA_CENTERS, + _AREAS_BY_ZONE, + _GPS_JITTER, + ) + + gen = self._make_generator() + _, (lng, lat) = gen._pick_area_and_gps("rural", self._area_map()) + near = any( + abs(lng - _AREA_CENTERS[code][0]) <= _GPS_JITTER + 1e-6 + and abs(lat - _AREA_CENTERS[code][1]) <= _GPS_JITTER + 1e-6 + for code in _AREAS_BY_ZONE["rural"] + ) + self.assertTrue(near, "GPS point must be within jitter of a rural anchor") + + def test_peri_urban_picks_peri_urban_area(self): + """Peri-urban zone must pick from the peri-urban candidate areas.""" + from odoo.addons.spp_farmer_registry_demo.models.seeded_farm_generator import ( + _AREAS_BY_ZONE, + ) - def test_gps_rural_zone_within_bounds(self): - """Rural zone GPS must be within Philippine rural bounds.""" gen = self._make_generator() - lng, lat = gen._generate_gps_for_zone("rural") - self.assertGreaterEqual(lat, 7.0) - self.assertLessEqual(lat, 16.5) - self.assertGreaterEqual(lng, 120.3) - self.assertLessEqual(lng, 125.5) - - def test_gps_peri_urban_zone_within_bounds(self): - """Peri-urban zone GPS must be within peri-urban bounds.""" + area_map = self._area_map() + area_id, _ = gen._pick_area_and_gps("peri_urban", area_map) + peri_ids = {area_map[c] for c in _AREAS_BY_ZONE["peri_urban"]} + self.assertIn(area_id, peri_ids) + + def test_unknown_zone_defaults_to_rural(self): + """Unknown zone falls back to the rural candidate set.""" gen = self._make_generator() - lng, lat = gen._generate_gps_for_zone("peri_urban") - self.assertGreaterEqual(lat, 13.5) - self.assertLessEqual(lat, 15.5) - self.assertGreaterEqual(lng, 120.5) - self.assertLessEqual(lng, 121.5) - - def test_gps_unknown_zone_defaults_to_rural(self): - """Unknown zone should fall back to rural bounds.""" + area_id, gps = gen._pick_area_and_gps("unknown_zone", self._area_map()) + self.assertIsNotNone(gps) + self.assertIn(area_id, self._area_map().values()) + + def test_no_eligible_areas_returns_none(self): + """With no matching demo areas, pick returns (None, None).""" gen = self._make_generator() - lng, lat = gen._generate_gps_for_zone("unknown_zone") - self.assertGreaterEqual(lat, 7.0) - self.assertLessEqual(lat, 16.5) + area_id, gps = gen._pick_area_and_gps("rural", {}) + self.assertIsNone(area_id) + self.assertIsNone(gps) def test_gps_is_deterministic(self): - """Same seed must produce identical GPS coordinates.""" + """Same seed must produce identical area + GPS.""" + area_map = self._area_map() gen1 = self._make_generator(seed=77) gen2 = self._make_generator(seed=77) - gps1 = gen1._generate_gps_for_zone("rural") - gps2 = gen2._generate_gps_for_zone("rural") - self.assertEqual(gps1, gps2) + self.assertEqual( + gen1._pick_area_and_gps("rural", area_map), + gen2._pick_area_and_gps("rural", area_map), + ) @tagged("post_install", "-at_install") @@ -378,31 +411,32 @@ def test_resolve_species_is_cached(self): self.assertIn("rice_irrigated", gen._species_cache) def test_get_gender_id(self): - """Gender lookup should delegate to _get_vocab_code.""" + """Gender lookup maps the human label to its ISO 5218 numeric code. + + ``res.partner.gender_id`` is domain-locked to ISO 5218 + (`urn:iso:std:iso:5218`), where '1' = Male, '2' = Female. The helper + translates 'male' → the vocab code whose ``code`` is '1'. + """ gen = self._make_generator() - # Create gender vocab if not exists Vocabulary = self.env["spp.vocabulary"] VocabCode = self.env["spp.vocabulary.code"] - vocab = Vocabulary.search([("namespace_uri", "=", "urn:openspp:vocab:gender")], limit=1) + iso_ns = "urn:iso:std:iso:5218" + vocab = Vocabulary.search([("namespace_uri", "=", iso_ns)], limit=1) if not vocab: - vocab = Vocabulary.create( - { - "name": "Gender", - "namespace_uri": "urn:openspp:vocab:gender", - } - ) + vocab = Vocabulary.create({"name": "ISO 5218 Gender", "namespace_uri": iso_ns}) + if not VocabCode.search([("namespace_uri", "=", iso_ns), ("code", "=", "1")], limit=1): VocabCode.create( { "vocabulary_id": vocab.id, - "namespace_uri": "urn:openspp:vocab:gender", - "code": "male", + "namespace_uri": iso_ns, + "code": "1", "display": "Male", } ) gender_id = gen._get_gender_id("male") - if gender_id: - code = self.env["spp.vocabulary.code"].browse(gender_id) - self.assertEqual(code.code, "male") + self.assertTrue(gender_id, "'male' should resolve to the ISO 5218 code '1'") + code = self.env["spp.vocabulary.code"].browse(gender_id) + self.assertEqual(code.code, "1") def test_get_head_type_id(self): """Head type lookup should return ID and cache it.""" @@ -797,6 +831,11 @@ def test_create_land_records(self): """Generated farms should have land records with polygons.""" gen = self._make_generator() self._create_season() + # Land records are anchored to the farm's GPS point, which is only + # generated when the farm lands inside a known demo area. Seed one of + # the rural anchor areas so a GPS point (and thus a parcel) is created. + if not self.env["spp.area"].search([("code", "=", "PH-NUE")], limit=1): + self.env["spp.area"].create({"draft_name": "Nueva Ecija", "code": "PH-NUE"}) bp = { "id": "test_land_rec", @@ -974,10 +1013,11 @@ def test_generate_all_farms_assigns_areas(self): """Farms should be assigned to demo areas if any exist.""" gen = self._make_generator() - # Create a demo area - area = self.env["spp.area"].search([("code", "like", "PH-%")], limit=1) - if not area: - area = self.env["spp.area"].create({"draft_name": "Test Province", "code": "PH-TEST"}) + # Seed one of the known anchor areas. The farm is only assigned an + # area when its zone's candidate code (e.g. PH-NUE for rural) exists; + # an arbitrary PH-* code that isn't an anchor would never be picked. + if not self.env["spp.area"].search([("code", "=", "PH-NUE")], limit=1): + self.env["spp.area"].create({"draft_name": "Nueva Ecija", "code": "PH-NUE"}) bp = { "id": "test_area", diff --git a/spp_farmer_registry_demo/tests/test_story_change_requests.py b/spp_farmer_registry_demo/tests/test_story_change_requests.py index 5622e63a5..7a0c94d73 100644 --- a/spp_farmer_registry_demo/tests/test_story_change_requests.py +++ b/spp_farmer_registry_demo/tests/test_story_change_requests.py @@ -507,7 +507,7 @@ def test_cr_defs_have_valid_states(self): def test_cr_defs_have_valid_type_codes(self): """All CR type codes must be one of the known types.""" - valid_types = {"update_farm_details", "manage_farm_activity"} + valid_types = {"update_farm_details", "manage_farm_activity", "manage_farm_asset"} for story_id, cr_def in self.wizard.STORY_CHANGE_REQUESTS.items(): self.assertIn( cr_def["type_code"], diff --git a/spp_irrigation/security/ir.model.access.csv b/spp_irrigation/security/ir.model.access.csv index dfaebe8d8..27bb5ed85 100644 --- a/spp_irrigation/security/ir.model.access.csv +++ b/spp_irrigation/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_spp_irrigation_asset,SPP Irrigation Asset Access,model_spp_irrigation_asset,spp_irrigation.group_irrigation_manager,1,1,1,1 +access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Read,model_spp_irrigation_asset,spp_registry.group_registry_viewer,1,0,0,0 diff --git a/spp_programs/data/user_roles.xml b/spp_programs/data/user_roles.xml index 66b6df1ad..85d10dee5 100644 --- a/spp_programs/data/user_roles.xml +++ b/spp_programs/data/user_roles.xml @@ -38,6 +38,7 @@ Command.link(ref('spp_registry.group_registry_write')), Command.link(ref('spp_cel_domain.group_cel_domain_viewer')), Command.link(ref('spp_approval.group_approval_approver')), + Command.link(ref('job_worker.group_queue_job_manager')), ]" /> @@ -87,6 +88,7 @@ Command.link(ref('spp_registry.group_registry_viewer')), Command.link(ref('spp_registry.group_registry_write')), Command.link(ref('spp_approval.group_approval_approver')), + Command.link(ref('job_worker.group_queue_job_manager')), ]" /> From b47a63a3ce76a6875cd75bcbbeb37f5a146df35a Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Mon, 8 Jun 2026 16:22:55 +0800 Subject: [PATCH 21/23] fix(spp_farmer_registry_demo): repair demo load crash + farm-user cycle access (#915) - Load Farmer Demo aborted on the second program: clearing the wizard's flat cash-entitlement line with (5, 0, 0) orphaned it (entitlement_id = NULL) instead of deleting it, and the deferred write violated the NOT-NULL constraint on the next program's flush, aborting the whole transaction. Delete the lines with unlink() instead. - Farm users hit 'not allowed to access Cycle (spp.cycle)' when opening an enrolled farm: the registrant form lists entitlements with their cycle_id column, but spp.cycle read was missing from the registry-officer ACL (entitlements, cycle memberships and payments already had it). Add the read grant for spp.cycle. - Add regression tests: full multi-program build (reproduces the orphaned line that single-program tests missed) and farm-user spp.cycle read. --- .../models/farmer_demo_generator.py | 12 +++-- .../tests/test_demo_generator.py | 46 +++++++++++++++++++ spp_programs/security/ir.model.access.csv | 1 + 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index c562a458b..e5b963ec1 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -1926,9 +1926,15 @@ def _configure_entitlement_formula(self, program, program_def): return ir_fields = self.env["ir.model.fields"] - # (5, 0, 0) clears the flat line the wizard created so the formula - # lines are the single source of truth. - commands = [(5, 0, 0)] + # DELETE the flat line(s) the wizard seeded so the formula lines are + # the single source of truth. `entitlement_id` is a required (NOT + # NULL) FK, so the lines must be unlinked (DELETE) — a `(5, 0, 0)` + # clear orphans them instead (UPDATE entitlement_id = NULL) and the + # deferred write violates the constraint on the next program's flush, + # aborting the whole demo transaction. See OP#915 round 7. + entitlement_manager.entitlement_item_ids.unlink() + + commands = [] for spec in items_spec: vals = {"amount": spec["amount"]} field_name = spec.get("multiplier_field") diff --git a/spp_farmer_registry_demo/tests/test_demo_generator.py b/spp_farmer_registry_demo/tests/test_demo_generator.py index e6c614de6..330884ed8 100644 --- a/spp_farmer_registry_demo/tests/test_demo_generator.py +++ b/spp_farmer_registry_demo/tests/test_demo_generator.py @@ -671,3 +671,49 @@ def test_program_manager_demo_user_can_approve_and_enqueue(self): user.has_group("job_worker.group_queue_job_manager"), "Approver must have queue job manager rights to enqueue entitlement validation", ) + + def test_full_program_build_completes_without_orphans(self): + """Building every demo program in one transaction must not abort. + + Regression for OP#915 round 7: clearing the wizard's flat entitlement + line with a ``(5, 0, 0)`` command orphaned it (``entitlement_id`` set to + NULL) instead of deleting it, and the deferred write violated the + NOT-NULL constraint on the *next* program's flush — aborting the whole + demo. Single-program tests miss it because there is no later flush. + Build all programs and flush to prove the lines are deleted, not + orphaned. + """ + from odoo.addons.spp_farmer_registry_demo.models.demo_programs import ( + get_all_demo_programs, + ) + + wizard = self.Generator.create({"name": _unique("Full Program Build")}) + program_map = wizard._create_demo_programs_via_wizard() + # Force pending writes to hit the DB; the orphan bug surfaced here. + self.env.flush_all() + + expected = {p["name"] for p in get_all_demo_programs()} + self.assertEqual(set(program_map), expected, "Every demo program must be created") + + # Input Subsidy keeps exactly its two formula lines, both linked. + input_subsidy = program_map["Input Subsidy Program"] + items = input_subsidy.get_manager(input_subsidy.MANAGER_ENTITLEMENT).entitlement_item_ids + self.assertEqual(len(items), 2) + self.assertTrue(all(item.entitlement_id for item in items)) + + # Equipment Grant has no formula spec, so its flat wizard line stays. + equipment = program_map["Equipment Grant Program"] + eq_items = equipment.get_manager(equipment.MANAGER_ENTITLEMENT).entitlement_item_ids + self.assertEqual(len(eq_items), 1) + + def test_farm_user_can_read_program_cycles(self): + """A Farm User (registry officer) must be able to read spp.cycle. + + The registrant form lists a registrant's entitlements with their + ``cycle_id`` column. The officer already has read on entitlements and + cycle memberships; without spp.cycle read too, opening any enrolled + farm raised 'not allowed to access Cycle'. OP#915 round 7. + """ + officer = self.env.ref("spp_demo.demo_officer") + # Must not raise AccessError for a plain Farm User. + self.env["spp.cycle"].with_user(officer).search([], limit=1) diff --git a/spp_programs/security/ir.model.access.csv b/spp_programs/security/ir.model.access.csv index c4f38ed04..2bde4fc1c 100644 --- a/spp_programs/security/ir.model.access.csv +++ b/spp_programs/security/ir.model.access.csv @@ -107,6 +107,7 @@ access_spp_program_membership_registry_read,Program Membership Registry Read Acc access_spp_program_membership_registry_write,Program Membership Registry Write Access,spp_programs.model_spp_program_membership,spp_registry.group_registry_write,1,1,1,0 access_spp_program_membership_registrar,Program Membership Registrar Access,spp_programs.model_spp_program_membership,spp_registry.group_registry_officer,1,1,1,0 access_spp_cycle_membership_registrar,Cycle Membership Registrar Access,spp_programs.model_spp_cycle_membership,spp_registry.group_registry_officer,1,1,1,0 +access_spp_cycle_registrar,Cycle Registrar Access,spp_programs.model_spp_cycle,spp_registry.group_registry_officer,1,0,0,0 access_spp_entitlement_registry_read,Entitlement Registry Read Access,spp_programs.model_spp_entitlement,spp_registry.group_registry_read,1,0,0,0 access_spp_entitlement_registry_write,Entitlement Registry Write Access,spp_programs.model_spp_entitlement,spp_registry.group_registry_write,1,1,1,0 access_spp_entitlement_registrar,Entitlement Registrar Access,spp_programs.model_spp_entitlement,spp_registry.group_registry_officer,1,0,0,0 From 21790356e284c408e49dea6417ca6bc6303f90b4 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 17 Jun 2026 12:23:24 +0800 Subject: [PATCH 22/23] fix(farmer_demo): add a working program cycle approver user (#915) The round-7 demo shipped a Program Manager user to approve cycles, but a Program Manager is not a Cycle Approver: cycle approval is gated on the Cycle Approver functional role (the group the Approve Cycle button is shown to), and group_programs_manager does not imply group_programs_cycle_approver. So the manager could neither see nor pass the cycle approval check. - Add a dedicated 'cycle_approver' demo user holding the Program Cycle Approver role (which also carries queue-job manager rights, so approving a cycle does not hit the entitlement-validation queue access error). - Route the demo's cycle approval definition through group_programs_cycle_approver so the approver can actually approve cycles. - Grant the Cycle Approver group to SPP Admin so Load Farmer Demo approves cycles cleanly instead of falling back to a forced state write. - Add regression tests covering the approval-group wiring and group membership. --- .../data/approval_definitions.xml | 16 +++++-- spp_farmer_registry_demo/data/demo_users.xml | 42 +++++++++++++++++++ .../tests/test_demo_generator.py | 42 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/spp_farmer_registry_demo/data/approval_definitions.xml b/spp_farmer_registry_demo/data/approval_definitions.xml index 078390649..1657dc589 100644 --- a/spp_farmer_registry_demo/data/approval_definitions.xml +++ b/spp_farmer_registry_demo/data/approval_definitions.xml @@ -12,15 +12,25 @@ managers (a gap to be fixed in MIS via a separate ticket). --> - + - Farmer: Cycle Approval - Program Manager + Farmer: Cycle Approval - Cycle Approver group - + False True 3 diff --git a/spp_farmer_registry_demo/data/demo_users.xml b/spp_farmer_registry_demo/data/demo_users.xml index 40a9370c9..f83b32994 100644 --- a/spp_farmer_registry_demo/data/demo_users.xml +++ b/spp_farmer_registry_demo/data/demo_users.xml @@ -163,4 +163,46 @@ + + + + Cycle Approver + cycle_approver + cycle_approver@example.com + demo + + + + + + + + + diff --git a/spp_farmer_registry_demo/tests/test_demo_generator.py b/spp_farmer_registry_demo/tests/test_demo_generator.py index 330884ed8..7ad5929fb 100644 --- a/spp_farmer_registry_demo/tests/test_demo_generator.py +++ b/spp_farmer_registry_demo/tests/test_demo_generator.py @@ -717,3 +717,45 @@ def test_farm_user_can_read_program_cycles(self): officer = self.env.ref("spp_demo.demo_officer") # Must not raise AccessError for a plain Farm User. self.env["spp.cycle"].with_user(officer).search([], limit=1) + + +@tagged("post_install", "-at_install") +class TestCycleApproverDemoUser(TransactionCase): + """OP#915: the demo must ship a user who can actually approve program cycles. + + Cycle approval is gated on the Cycle Approver functional role (the group the + "Approve Cycle" button is shown to). A Program Manager is intentionally NOT a + Cycle Approver, so the demo provides a dedicated `cycle_approver` user and + routes the cycle approval definition through that group. + """ + + def test_cycle_approval_definition_uses_cycle_approver_group(self): + definition = self.env.ref("spp_farmer_registry_demo.approval_definition_farmer_cycle_manager") + self.assertEqual(definition.approval_type, "group") + self.assertEqual( + definition.approval_group_id, + self.env.ref("spp_programs.group_programs_cycle_approver"), + "Cycle approval must route through the Cycle Approver group so the dedicated approver can approve cycles.", + ) + + def test_cycle_approver_user_is_in_cycle_approver_group(self): + user = self.env.ref("spp_farmer_registry_demo.demo_user_cycle_approver") + self.assertTrue( + user.has_group("spp_programs.group_programs_cycle_approver"), + "The demo cycle approver must hold the Cycle Approver group so the " + "Approve Cycle button is shown and approval is authorized.", + ) + # The role also carries queue-job manager rights so approving a cycle + # (which enqueues entitlement validation) does not hit an access error. + self.assertTrue(user.has_group("job_worker.group_queue_job_manager")) + + def test_admin_can_approve_cycles_for_clean_demo_load(self): + """SPP Admin (the demo loader) must be in the Cycle Approver group so + Load Farmer Demo approves cycles without falling back to a forced state + write (which logs a spurious traceback).""" + admin_group = self.env.ref("spp_security.group_spp_admin") + self.assertIn( + self.env.ref("spp_programs.group_programs_cycle_approver"), + admin_group.implied_ids, + "SPP Admin must imply the Cycle Approver group for clean demo loading.", + ) From bf54367088c3ebb487afee62c0b33b2487a8a492 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Mon, 22 Jun 2026 10:03:46 +0800 Subject: [PATCH 23/23] chore(farmer_demo): regenerate READMEs and format irrigation view (#915) Pre-commit fixes for CI: regenerate the spp_farmer_registry_demo and spp_irrigation README/description from fragments, and apply prettier formatting to spp_irrigation/views/irrigation_view.xml. --- spp_farmer_registry_demo/README.rst | 18 ++++++++++++++++++ .../static/description/index.html | 19 +++++++++++++++++++ spp_irrigation/README.rst | 8 ++++++++ spp_irrigation/static/description/index.html | 9 +++++++++ spp_irrigation/views/irrigation_view.xml | 5 +---- 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/spp_farmer_registry_demo/README.rst b/spp_farmer_registry_demo/README.rst index d4db0baeb..ad80e7a35 100644 --- a/spp_farmer_registry_demo/README.rst +++ b/spp_farmer_registry_demo/README.rst @@ -120,6 +120,24 @@ Dependencies Changelog ========= +19.0.2.1.0 +~~~~~~~~~~ + +- feat(demo): add GIS + irrigation scenario (FM4) with reservoir + canal + network seed; FM4's idle hectare is now narratively explained as the + downstream consequence of reduced reservoir capacity +- feat(demo): seed farm assets (hand tractor on FM1, water pump on FM8) + and a ``manage_farm_asset`` change request in the CR lifecycle +- feat(demo): add a closed prior-year farm season alongside the active + one to demonstrate the ``draft → active → closed`` state machine +- chore(deps): declare ``spp_gis``, ``spp_land_record``, + ``spp_irrigation``, ``spp_farmer_registry_vocabularies`` explicitly — + these were used at runtime but never listed +- docs(demo): add Scenario 10 (GIS + irrigation walk for FM4); document + AGROVOC species selection (rice / tilapia) in Scenario 1; add + farm-season state-machine sub-step; refresh FM1/FM4/FM8 farm story + tables and the CR overview + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_farmer_registry_demo/static/description/index.html b/spp_farmer_registry_demo/static/description/index.html index b86e8660f..d7c772f76 100644 --- a/spp_farmer_registry_demo/static/description/index.html +++ b/spp_farmer_registry_demo/static/description/index.html @@ -488,6 +488,25 @@

Changelog

+

19.0.2.1.0

+
    +
  • feat(demo): add GIS + irrigation scenario (FM4) with reservoir + canal +network seed; FM4’s idle hectare is now narratively explained as the +downstream consequence of reduced reservoir capacity
  • +
  • feat(demo): seed farm assets (hand tractor on FM1, water pump on FM8) +and a manage_farm_asset change request in the CR lifecycle
  • +
  • feat(demo): add a closed prior-year farm season alongside the active +one to demonstrate the draft → active → closed state machine
  • +
  • chore(deps): declare spp_gis, spp_land_record, +spp_irrigation, spp_farmer_registry_vocabularies explicitly — +these were used at runtime but never listed
  • +
  • docs(demo): add Scenario 10 (GIS + irrigation walk for FM4); document +AGROVOC species selection (rice / tilapia) in Scenario 1; add +farm-season state-machine sub-step; refresh FM1/FM4/FM8 farm story +tables and the CR overview
  • +
+
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_irrigation/README.rst b/spp_irrigation/README.rst index c3805dc15..06303904c 100644 --- a/spp_irrigation/README.rst +++ b/spp_irrigation/README.rst @@ -100,6 +100,14 @@ Dependencies Changelog ========= +19.0.2.1.0 +~~~~~~~~~~ + +- feat(views): add an "Irrigation" tab on the farm (group) form so + per-farm irrigation assets are reachable without leaving the farm + record; backed by a new ``irrigation_asset_ids`` One2many on + ``res.partner`` (inverse of the existing ``farm_id``) + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_irrigation/static/description/index.html b/spp_irrigation/static/description/index.html index a6ecb2675..355f9974f 100644 --- a/spp_irrigation/static/description/index.html +++ b/spp_irrigation/static/description/index.html @@ -468,6 +468,15 @@

    Changelog

+

19.0.2.1.0

+
    +
  • feat(views): add an “Irrigation” tab on the farm (group) form so +per-farm irrigation assets are reachable without leaving the farm +record; backed by a new irrigation_asset_ids One2many on +res.partner (inverse of the existing farm_id)
  • +
+
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_irrigation/views/irrigation_view.xml b/spp_irrigation/views/irrigation_view.xml index 387109dbf..4a02b2aaa 100644 --- a/spp_irrigation/views/irrigation_view.xml +++ b/spp_irrigation/views/irrigation_view.xml @@ -105,10 +105,7 @@ - +