@@ -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
36643920class SPPMISDemoWizard (models .TransientModel ):
36653921 """Wizard interface for MIS Demo Generator."""
0 commit comments