@@ -24,6 +24,15 @@ def compute_breakdown(self, registrant_ids, group_by, statistics=None, context=N
2424
2525 Uses dimension cache for 5-10x performance improvement.
2626
27+ Expansion semantics (deliberate): if ANY requested dimension has
28+ ``applies_to == "individuals"``, the ENTIRE registrant set is expanded
29+ from groups to their active individual members before evaluation —
30+ including for any non-individual dimensions mixed into the same
31+ request (mixing scopes in one breakdown is inherently ambiguous; the
32+ expansion is all-or-nothing by design). Consequently breakdown totals
33+ count members and intentionally need not reconcile with a group-level
34+ scope count.
35+
2736 :param registrant_ids: List of partner IDs
2837 :param group_by: List of dimension names
2938 :param statistics: List of statistic names (optional)
@@ -56,6 +65,14 @@ def compute_breakdown(self, registrant_ids, group_by, statistics=None, context=N
5665 if not dimensions :
5766 return {}
5867
68+ # Auto-expand groups to members when any dimension applies to individuals only
69+ needs_expansion = any (d .applies_to == "individuals" for d in dimensions )
70+ if needs_expansion :
71+ registrant_ids = self ._expand_groups_to_members (registrant_ids )
72+
73+ if not registrant_ids :
74+ return {}
75+
5976 # Get cache service
6077 cache_service = self .env ["spp.metric.dimension.cache" ]
6178
@@ -95,3 +112,41 @@ def compute_breakdown(self, registrant_ids, group_by, statistics=None, context=N
95112 # TODO: Add per-cell statistics if needed
96113
97114 return breakdown
115+
116+ @api .model
117+ def _expand_groups_to_members (self , registrant_ids ):
118+ """
119+ Expand group IDs to their individual member IDs.
120+
121+ Groups are replaced by their active members. Individual IDs pass through.
122+ The result is deduplicated.
123+
124+ :param registrant_ids: List of partner IDs (groups and/or individuals)
125+ :returns: Deduplicated list of individual partner IDs
126+ :rtype: list
127+ """
128+ # sudo: aggregate breakdown metrics must expand groups to their members
129+ # across all registrants regardless of the caller's record rules.
130+ # Read-only (no writes); callers are authorized at the service entry point.
131+ Partner = self .env ["res.partner" ].sudo () # nosemgrep: odoo-sudo-without-context,odoo-sudo-on-sensitive-models
132+ records = Partner .browse (registrant_ids ).exists ()
133+
134+ groups = records .filtered ("is_group" )
135+ group_ids = groups .ids
136+ individual_ids = set ((records - groups ).ids )
137+
138+ if not group_ids :
139+ return list (individual_ids )
140+
141+ # Expand groups via active memberships
142+ Membership = self .env ["spp.group.membership" ].sudo () # nosemgrep: odoo-sudo-without-context
143+ memberships = Membership .search (
144+ [
145+ ("group" , "in" , group_ids ),
146+ ("is_ended" , "=" , False ),
147+ ]
148+ )
149+
150+ individual_ids .update (memberships .individual .ids )
151+
152+ return list (individual_ids )
0 commit comments