Skip to content

Commit b726a8b

Browse files
committed
feat(spp_mis_demo_v2): add geographic data loading and GIS report refresh
Port the geographic data pipeline from the internal branch: - Add load_geographic_data/country_code wizard fields and presets - Add _load_geographic_data() to load area shapes via spp_demo loader - Add _assign_registrant_areas() to assign municipalities to registrants - Add _generate_coordinates() for GPS points within area polygons - Add _refresh_gis_reports() to populate report data immediately - Add geographic data summary to success notification - Add spp_registrant_gis dependency for GPS coordinate field Without this, GIS reports are empty and QGIS plugin queries return no data.
1 parent 94deba0 commit b726a8b

File tree

3 files changed

+262
-0
lines changed

3 files changed

+262
-0
lines changed

spp_mis_demo_v2/__manifest__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"spp_demo",
2121
# GIS Reports for geographic visualization
2222
"spp_gis_report",
23+
# Registrant GPS coordinates for QGIS plugin demo
24+
"spp_registrant_gis",
2325
# Statistics and aggregation for demo indicators
2426
"spp_statistic",
2527
"spp_aggregation",

spp_mis_demo_v2/models/mis_demo_generator.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,23 @@ class SPPMISDemoGenerator(models.TransientModel):
181181
help="Generate QR credentials for demo story personas (Maria Santos, etc.)",
182182
)
183183

184+
# Geographic data options
185+
load_geographic_data = fields.Boolean(
186+
string="Load Geographic Data",
187+
default=True,
188+
help="Load area data with GIS shapes and assign GPS coordinates to registrants for QGIS plugin demo",
189+
)
190+
country_code = fields.Selection(
191+
[
192+
("phl", "Philippines"),
193+
("lka", "Sri Lanka"),
194+
("tgo", "Togo"),
195+
],
196+
string="Country",
197+
default="phl",
198+
help="Country for geographic data (areas and GIS shapes)",
199+
)
200+
184201
# Locale settings
185202
locale_origin = fields.Many2one(
186203
"res.country",
@@ -256,6 +273,8 @@ def _onchange_demo_mode(self):
256273
"case_volume_count": 10,
257274
"generate_claim169_demo": True,
258275
"generate_credentials_for_stories": True,
276+
"load_geographic_data": True,
277+
"country_code": "phl",
259278
},
260279
"training": {
261280
"create_demo_programs": True,
@@ -278,6 +297,8 @@ def _onchange_demo_mode(self):
278297
"case_volume_count": 25,
279298
"generate_claim169_demo": True,
280299
"generate_credentials_for_stories": True,
300+
"load_geographic_data": True,
301+
"country_code": "phl",
281302
},
282303
"testing": {
283304
"create_demo_programs": True,
@@ -300,6 +321,8 @@ def _onchange_demo_mode(self):
300321
"case_volume_count": 200,
301322
"generate_claim169_demo": True,
302323
"generate_credentials_for_stories": True,
324+
"load_geographic_data": True,
325+
"country_code": "phl",
303326
},
304327
"complete": {
305328
"create_demo_programs": True,
@@ -322,6 +345,8 @@ def _onchange_demo_mode(self):
322345
"case_volume_count": 50,
323346
"generate_claim169_demo": True,
324347
"generate_credentials_for_stories": True,
348+
"load_geographic_data": True,
349+
"country_code": "phl",
325350
},
326351
}
327352
defaults = mode_defaults.get(self.demo_mode, mode_defaults["sales"])
@@ -453,6 +478,13 @@ def action_generate(self):
453478
self._create_test_personas()
454479
stats["test_personas_created"] = True
455480

481+
# Step 0.4: Load geographic data (if enabled)
482+
if self.load_geographic_data:
483+
_logger.info(f"Loading geographic data for {self.country_code}...")
484+
geo_result = self._load_geographic_data(stats)
485+
if geo_result:
486+
stats["areas_loaded"] = geo_result.get("shapes_loaded", 0)
487+
456488
# Step 0.5: Ensure demo stories exist (auto-generate if needed)
457489
stories_created = self._ensure_demo_stories_exist(stats)
458490
if stories_created:
@@ -522,6 +554,16 @@ def action_generate(self):
522554
_logger.info("Generating Claim 169 demo data...")
523555
self._generate_claim169_demo(stats)
524556

557+
# Step 11: Assign areas and generate GPS coordinates (if geographic data loaded)
558+
if self.load_geographic_data:
559+
_logger.info("Assigning areas to registrants...")
560+
self._assign_registrant_areas(stats)
561+
_logger.info("Generating GPS coordinates for registrants...")
562+
self._generate_coordinates(stats)
563+
564+
# Step 12: Refresh GIS reports so map data is available immediately
565+
self._refresh_gis_reports(stats)
566+
525567
self.state = "completed"
526568

527569
# Return success notification with detailed summary
@@ -3618,6 +3660,9 @@ def _show_success_notification(self, stats):
36183660
if claim169_parts:
36193661
message_parts.append(_("QR Credentials: %s created") % ", ".join(claim169_parts))
36203662

3663+
# Geographic Data
3664+
self._append_geographic_summary(stats, message_parts)
3665+
36213666
# Warnings
36223667
if stats["missing_registrants"]:
36233668
message_parts.append("")
@@ -3660,6 +3705,217 @@ def _show_success_notification(self, stats):
36603705
},
36613706
}
36623707

3708+
def _append_geographic_summary(self, stats, message_parts):
3709+
"""Append geographic data summary to notification message parts."""
3710+
if not self.load_geographic_data:
3711+
return
3712+
geo_parts = []
3713+
if stats.get("areas_loaded", 0) > 0:
3714+
geo_parts.append(_("%(count)s areas with GIS shapes", count=stats["areas_loaded"]))
3715+
if stats.get("areas_assigned", 0) > 0:
3716+
geo_parts.append(_("%(count)s groups assigned to areas", count=stats["areas_assigned"]))
3717+
if stats.get("coordinates_generated", 0) > 0:
3718+
geo_parts.append(_("%(count)s registrants with GPS coordinates", count=stats["coordinates_generated"]))
3719+
if geo_parts:
3720+
message_parts.append(_("Geographic Data: %s") % ", ".join(geo_parts))
3721+
3722+
def _load_geographic_data(self, stats):
3723+
"""Load geographic area data with GIS shapes.
3724+
3725+
Uses the DemoAreaLoader from spp_demo to load country-specific
3726+
area hierarchies with GIS polygon data for spatial queries.
3727+
3728+
Args:
3729+
stats: Statistics dictionary to update
3730+
3731+
Returns:
3732+
dict: Result with counts of loaded data
3733+
"""
3734+
try:
3735+
loader = self.env["spp.demo.area.loader"]
3736+
result = loader.load_country_areas(self.country_code, load_shapes=True)
3737+
_logger.info(
3738+
"[spp.mis.demo] Loaded geographic data for %s: %d areas with GIS shapes",
3739+
self.country_code,
3740+
result.get("shapes_loaded", 0),
3741+
)
3742+
return result
3743+
except Exception as e:
3744+
_logger.warning("[spp.mis.demo] Failed to load geographic data: %s", e)
3745+
return None
3746+
3747+
def _assign_registrant_areas(self, stats):
3748+
"""Assign geographic areas to registrants.
3749+
3750+
Strategy:
3751+
- Get all municipalities (level 3 areas) from the loaded country
3752+
- For each group, assign a random municipality to area_id
3753+
- Individual members inherit area_id from their group
3754+
3755+
Args:
3756+
stats: Statistics dictionary to update
3757+
"""
3758+
Area = self.env["spp.area"]
3759+
Partner = self.env["res.partner"]
3760+
3761+
# Get all level 3 areas (municipalities) that have geo_polygon data
3762+
municipalities = Area.search([("area_level", "=", 3), ("geo_polygon", "!=", False)])
3763+
3764+
if not municipalities:
3765+
_logger.warning("[spp.mis.demo] No municipalities with GIS data found, skipping area assignment")
3766+
stats["areas_assigned"] = 0
3767+
return
3768+
3769+
_logger.info("[spp.mis.demo] Found %d municipalities with GIS data", len(municipalities))
3770+
3771+
# Get all groups (households)
3772+
groups = Partner.search([("is_group", "=", True), ("is_registrant", "=", True)])
3773+
3774+
if not groups:
3775+
_logger.warning("[spp.mis.demo] No groups found, skipping area assignment")
3776+
stats["areas_assigned"] = 0
3777+
return
3778+
3779+
# Assign random municipality to each group
3780+
groups_assigned = 0
3781+
for group in groups:
3782+
municipality = random.choice(municipalities)
3783+
group.write({"area_id": municipality.id})
3784+
groups_assigned += 1
3785+
3786+
# Members inherit area from group
3787+
members = Partner.search([("group_membership_ids.group", "=", group.id)])
3788+
if members:
3789+
members.write({"area_id": municipality.id})
3790+
3791+
stats["areas_assigned"] = groups_assigned
3792+
_logger.info("[spp.mis.demo] Assigned areas to %d groups", groups_assigned)
3793+
3794+
def _generate_coordinates(self, stats):
3795+
"""Generate GPS coordinates for registrants.
3796+
3797+
For each registrant with an area_id that has geo_polygon data,
3798+
generates a random point within the area polygon and sets
3799+
the coordinates field (if spp_registrant_gis is installed).
3800+
3801+
Uses shapely to generate random points within polygons.
3802+
3803+
Args:
3804+
stats: Statistics dictionary to update
3805+
"""
3806+
# Check if spp_registrant_gis is installed
3807+
if "coordinates" not in self.env["res.partner"]._fields:
3808+
_logger.info("[spp.mis.demo] spp_registrant_gis not installed, skipping coordinate generation")
3809+
stats["coordinates_generated"] = 0
3810+
return
3811+
3812+
try:
3813+
from shapely.geometry import shape
3814+
from shapely.wkb import loads as wkbloads
3815+
except ImportError:
3816+
_logger.warning("[spp.mis.demo] shapely not available, skipping coordinate generation")
3817+
stats["coordinates_generated"] = 0
3818+
return
3819+
3820+
Partner = self.env["res.partner"]
3821+
Area = self.env["spp.area"]
3822+
3823+
# Get all registrants with an area_id
3824+
registrants = Partner.search(
3825+
[
3826+
("is_registrant", "=", True),
3827+
("area_id", "!=", False),
3828+
]
3829+
)
3830+
3831+
if not registrants:
3832+
_logger.warning("[spp.mis.demo] No registrants with areas found")
3833+
stats["coordinates_generated"] = 0
3834+
return
3835+
3836+
_logger.info("[spp.mis.demo] Generating coordinates for %d registrants", len(registrants))
3837+
3838+
coordinates_generated = 0
3839+
3840+
# Group registrants by area to minimize queries
3841+
registrants_by_area = {}
3842+
for registrant in registrants:
3843+
area_id = registrant.area_id.id
3844+
if area_id not in registrants_by_area:
3845+
registrants_by_area[area_id] = []
3846+
registrants_by_area[area_id].append(registrant)
3847+
3848+
# Process each area
3849+
for area_id, area_registrants in registrants_by_area.items():
3850+
area = Area.browse(area_id)
3851+
3852+
# Skip if no polygon data
3853+
if not area.geo_polygon:
3854+
continue
3855+
3856+
try:
3857+
# Convert WKB to shapely polygon
3858+
polygon = wkbloads(bytes(area.geo_polygon.data))
3859+
3860+
# Generate random points for all registrants in this area
3861+
minx, miny, maxx, maxy = polygon.bounds
3862+
3863+
for registrant in area_registrants:
3864+
# Generate random point within bounding box, retry if outside polygon
3865+
max_attempts = 10
3866+
for _attempt in range(max_attempts):
3867+
point_x = random.uniform(minx, maxx)
3868+
point_y = random.uniform(miny, maxy)
3869+
point = shape({"type": "Point", "coordinates": [point_x, point_y]})
3870+
3871+
if polygon.contains(point):
3872+
# Set the coordinates field (GeoPointField expects WKB)
3873+
registrant.write(
3874+
{
3875+
"coordinates": f"POINT({point_x} {point_y})",
3876+
}
3877+
)
3878+
coordinates_generated += 1
3879+
break
3880+
else:
3881+
# If we couldn't find a point inside after max_attempts, use centroid
3882+
centroid = polygon.centroid
3883+
registrant.write(
3884+
{
3885+
"coordinates": f"POINT({centroid.x} {centroid.y})",
3886+
}
3887+
)
3888+
coordinates_generated += 1
3889+
3890+
except Exception as e:
3891+
_logger.warning("[spp.mis.demo] Failed to generate coordinates for area %s: %s", area.name, e)
3892+
continue
3893+
3894+
stats["coordinates_generated"] = coordinates_generated
3895+
_logger.info("[spp.mis.demo] Generated coordinates for %d registrants", coordinates_generated)
3896+
3897+
def _refresh_gis_reports(self, stats):
3898+
"""Refresh all active GIS reports so map data is available immediately."""
3899+
GISReport = self.env["spp.gis.report"]
3900+
reports = GISReport.search([("active", "=", True)])
3901+
3902+
if not reports:
3903+
_logger.info("[spp.mis.demo] No active GIS reports found to refresh")
3904+
stats["gis_reports_refreshed"] = 0
3905+
return
3906+
3907+
refreshed = 0
3908+
for report in reports:
3909+
try:
3910+
report._refresh_data()
3911+
refreshed += 1
3912+
_logger.info("[spp.mis.demo] Refreshed GIS report: %s", report.name)
3913+
except Exception:
3914+
_logger.exception("[spp.mis.demo] Failed to refresh GIS report: %s", report.name)
3915+
3916+
stats["gis_reports_refreshed"] = refreshed
3917+
_logger.info("[spp.mis.demo] Refreshed %d GIS reports", refreshed)
3918+
36633919

36643920
class SPPMISDemoWizard(models.TransientModel):
36653921
"""Wizard interface for MIS Demo Generator."""

spp_mis_demo_v2/views/mis_demo_wizard_view.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
<li>Fairness analysis data</li>
2525
</ul>
2626
</div>
27+
<group string="Geographic Data">
28+
<field name="load_geographic_data"/>
29+
<field name="country_code" invisible="not load_geographic_data"/>
30+
</group>
2731
</sheet>
2832
<footer>
2933
<button

0 commit comments

Comments
 (0)