@@ -467,8 +467,31 @@ def _compute_base_aggregation(self):
467467 _logger .info ("No areas at level %s found" , self .base_area_level )
468468 return {}
469469
470- # Add area filter to domain
471- domain .append ((area_field , "in" , base_areas .ids ))
470+ # Build mapping from descendant areas to their base-level ancestor.
471+ # Registrants may be assigned to areas more granular than base_area_level
472+ # (e.g., barangays when base level is municipality). We include all
473+ # descendant areas in the query and aggregate results back to the
474+ # base-level parent.
475+ #
476+ # Fetch all descendants of all base areas in a single query, then build
477+ # the child_to_base mapping in memory to avoid an N+1 query pattern.
478+ child_to_base = {base_area .id : base_area .id for base_area in base_areas }
479+ all_descendants = self .env ["spp.area" ].search (
480+ [
481+ ("id" , "child_of" , base_areas .ids ),
482+ ("id" , "not in" , base_areas .ids ),
483+ ]
484+ )
485+ # For each descendant, walk up its parent chain to find the base-level ancestor
486+ for desc in all_descendants :
487+ ancestor = desc .parent_id
488+ while ancestor and ancestor .id not in child_to_base :
489+ ancestor = ancestor .parent_id
490+ if ancestor :
491+ child_to_base [desc .id ] = child_to_base [ancestor .id ]
492+
493+ # Add area filter to domain (base areas + all descendants)
494+ domain .append ((area_field , "in" , list (child_to_base .keys ())))
472495
473496 # Initialize results for all base areas with 0
474497 # This ensures areas with no matching records get 0 instead of being missing
@@ -484,12 +507,11 @@ def _compute_base_aggregation(self):
484507 for group in groups :
485508 if group [area_field ]:
486509 area_id = group [area_field ][0 ]
510+ base_id = child_to_base .get (area_id , area_id )
487511 count = group [f"{ area_field } _count" ]
488- results [area_id ] = {
489- "raw" : count ,
490- "count" : count ,
491- "weight" : count ,
492- }
512+ results [base_id ]["raw" ] += count
513+ results [base_id ]["count" ] += count
514+ results [base_id ]["weight" ] += count
493515
494516 elif self .aggregation_method in ("sum" , "avg" , "min" , "max" ):
495517 if not self .aggregation_field :
@@ -506,13 +528,37 @@ def _compute_base_aggregation(self):
506528 for group in groups :
507529 if group [area_field ]:
508530 area_id = group [area_field ][0 ]
531+ base_id = child_to_base .get (area_id , area_id )
509532 value = group .get (self .aggregation_field ) or 0
510533 count = group [f"{ area_field } _count" ]
511- results [area_id ] = {
512- "raw" : value ,
513- "count" : count ,
514- "weight" : count ,
515- }
534+ if agg_func == "avg" :
535+ # Accumulate weighted sum so we can compute a proper
536+ # weighted average across subgroups when rolling up.
537+ results [base_id ]["raw" ] += value * count
538+ elif agg_func == "sum" :
539+ # read_group already returns the sum for the subgroup;
540+ # multiplying by count would inflate the total.
541+ results [base_id ]["raw" ] += value
542+ elif agg_func == "min" :
543+ # Keep the lowest value seen across subgroups.
544+ if results [base_id ]["count" ] == 0 :
545+ results [base_id ]["raw" ] = value
546+ else :
547+ results [base_id ]["raw" ] = min (results [base_id ]["raw" ], value )
548+ elif agg_func == "max" :
549+ # Keep the highest value seen across subgroups.
550+ if results [base_id ]["count" ] == 0 :
551+ results [base_id ]["raw" ] = value
552+ else :
553+ results [base_id ]["raw" ] = max (results [base_id ]["raw" ], value )
554+ results [base_id ]["count" ] += count
555+ results [base_id ]["weight" ] += count
556+
557+ # For avg: convert accumulated weighted sum back to weighted average
558+ if agg_func == "avg" :
559+ for data in results .values ():
560+ if data ["count" ] > 0 :
561+ data ["raw" ] = data ["raw" ] / data ["count" ]
516562
517563 # Fill in None for areas with no data (distinguishes "no data" from "zero count")
518564 for area in base_areas :
@@ -1478,14 +1524,17 @@ def _to_geojson(
14781524
14791525 # Add geometry if requested
14801526 if include_geometry and data .area_id .geo_polygon :
1481- # TODO: Use PostGIS ST_AsGeoJSON for performance
1482- # For now, use Odoo's geometry field (WKT format)
14831527 try :
1484- from shapely import wkt
1528+ geo = data .area_id .geo_polygon
1529+ # GeoPolygonField may return a Shapely geometry object
1530+ # or a WKT/WKB string depending on the spp_gis version.
1531+ if hasattr (geo , "__geo_interface__" ):
1532+ feature ["geometry" ] = geo .__geo_interface__
1533+ else :
1534+ from shapely import wkt
14851535
1486- shape = wkt .loads (data .area_id .geo_polygon )
1487- # __geo_interface__ returns a dict that's already JSON-serializable
1488- feature ["geometry" ] = shape .__geo_interface__
1536+ shape = wkt .loads (geo )
1537+ feature ["geometry" ] = shape .__geo_interface__
14891538 except ImportError :
14901539 _logger .warning (
14911540 "shapely not available, geometry export limited. Install shapely for full geometry support."
0 commit comments