diff --git a/spp_programs/README.rst b/spp_programs/README.rst index 61b3d267..fc223252 100644 --- a/spp_programs/README.rst +++ b/spp_programs/README.rst @@ -254,6 +254,18 @@ Dependencies Changelog ========= +19.0.2.0.9 +~~~~~~~~~~ + +- Add context flags (``skip_registrant_statistics``, + ``skip_program_statistics``) to suppress expensive computed field + recomputation during bulk operations +- Add ``refresh_beneficiary_counts()`` on program and + ``refresh_statistics()`` on cycle for one-shot recomputation after + bulk operations +- Replace ``bool(rec.program_membership_ids)`` with SQL query in + ``_compute_has_members`` + 19.0.2.0.8 ~~~~~~~~~~ diff --git a/spp_programs/__manifest__.py b/spp_programs/__manifest__.py index 1de62a0c..b32ff972 100644 --- a/spp_programs/__manifest__.py +++ b/spp_programs/__manifest__.py @@ -4,7 +4,7 @@ "name": "OpenSPP Programs", "summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.", "category": "OpenSPP/Core", - "version": "19.0.2.0.8", + "version": "19.0.2.0.9", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_programs/models/cycle.py b/spp_programs/models/cycle.py index d4ae4351..07c4a85b 100644 --- a/spp_programs/models/cycle.py +++ b/spp_programs/models/cycle.py @@ -275,6 +275,16 @@ def _compute_entitlements_count(self): entitlements_count = self.env["spp.entitlement"].search_count([("cycle_id", "=", rec.id)]) rec.entitlements_count = entitlements_count + def refresh_statistics(self): + """Refresh all cycle statistics after bulk operations. + + Call this after raw SQL inserts that bypass ORM dependency tracking + (e.g. bulk_create_memberships with skip_duplicates=True). + """ + self._compute_members_count() + self._compute_entitlements_count() + self._compute_total_entitlements_count() + @api.depends("entitlement_ids", "inkind_entitlement_ids") def _compute_total_entitlements_count(self): if not self.ids: diff --git a/spp_programs/models/managers/cycle_manager_base.py b/spp_programs/models/managers/cycle_manager_base.py index 22e6a0b1..c3914b96 100644 --- a/spp_programs/models/managers/cycle_manager_base.py +++ b/spp_programs/models/managers/cycle_manager_base.py @@ -326,8 +326,8 @@ def mark_import_as_done(self, cycle, msg): cycle.locked_reason = None cycle.message_post(body=msg) - # Update Statistics - cycle._compute_members_count() + # Refresh statistics after bulk operations + cycle.refresh_statistics() def mark_prepare_entitlement_as_done(self, cycle, msg): """Complete the preparation of entitlements. diff --git a/spp_programs/models/managers/eligibility_manager.py b/spp_programs/models/managers/eligibility_manager.py index 5392af4a..54dde250 100644 --- a/spp_programs/models/managers/eligibility_manager.py +++ b/spp_programs/models/managers/eligibility_manager.py @@ -165,8 +165,7 @@ def _import_registrants_async(self, new_beneficiaries, state="draft"): def mark_import_as_done(self): self.ensure_one() - self.program_id._compute_eligible_beneficiary_count() - self.program_id._compute_beneficiary_count() + self.program_id.refresh_beneficiary_counts() self.program_id.is_locked = False self.program_id.locked_reason = None diff --git a/spp_programs/models/programs.py b/spp_programs/models/programs.py index 0aa25ec9..1888ef05 100644 --- a/spp_programs/models/programs.py +++ b/spp_programs/models/programs.py @@ -187,8 +187,23 @@ def _check_unique_program_name(self): @api.depends("program_membership_ids") def _compute_has_members(self): + if self.env.context.get("skip_program_statistics"): + return + if not self.ids: + for rec in self: + rec.has_members = False + return + self.env.cr.execute( + """ + SELECT program_id FROM spp_program_membership + WHERE program_id IN %s + GROUP BY program_id + """, + (tuple(self.ids),), + ) + programs_with_members = {row[0] for row in self.env.cr.fetchall()} for rec in self: - rec.has_members = bool(rec.program_membership_ids) + rec.has_members = rec.id in programs_with_members @api.depends("compliance_manager_ids", "compliance_manager_ids.manager_ref_id") def _compute_has_compliance_criteria(self): @@ -273,6 +288,16 @@ def _compute_beneficiary_count(self): count = rec.count_beneficiaries(None)["value"] rec.update({"beneficiaries_count": count}) + def refresh_beneficiary_counts(self): + """Refresh all beneficiary statistics after bulk operations. + + Call this after raw SQL inserts that bypass ORM dependency tracking + (e.g. bulk_create_memberships with skip_duplicates=True). + """ + self._compute_beneficiary_count() + self._compute_eligible_beneficiary_count() + self._compute_has_members() + @api.depends("cycle_ids") def _compute_cycle_count(self): for rec in self: diff --git a/spp_programs/models/registrant.py b/spp_programs/models/registrant.py index 15dd4885..20a8cd31 100644 --- a/spp_programs/models/registrant.py +++ b/spp_programs/models/registrant.py @@ -43,6 +43,8 @@ def _compute_total_entitlements_count(self): @api.depends("program_membership_ids") def _compute_program_membership_count(self): """Batch-efficient program membership count using read_group.""" + if self.env.context.get("skip_registrant_statistics"): + return if not self: return @@ -66,6 +68,8 @@ def _compute_program_membership_count(self): @api.depends("entitlement_ids") def _compute_entitlements_count(self): """Batch-efficient entitlements count using _read_group.""" + if self.env.context.get("skip_registrant_statistics"): + return if not self: return @@ -89,6 +93,8 @@ def _compute_entitlements_count(self): @api.depends("cycle_ids") def _compute_cycle_count(self): """Batch-efficient cycle membership count using _read_group.""" + if self.env.context.get("skip_registrant_statistics"): + return if not self: return @@ -112,6 +118,8 @@ def _compute_cycle_count(self): @api.depends("inkind_entitlement_ids") def _compute_inkind_entitlements_count(self): """Batch-efficient in-kind entitlements count using _read_group.""" + if self.env.context.get("skip_registrant_statistics"): + return if not self: return diff --git a/spp_programs/readme/HISTORY.md b/spp_programs/readme/HISTORY.md index 2e335856..bd3fbc33 100644 --- a/spp_programs/readme/HISTORY.md +++ b/spp_programs/readme/HISTORY.md @@ -1,3 +1,9 @@ +### 19.0.2.0.9 + +- Add context flags (`skip_registrant_statistics`, `skip_program_statistics`) to suppress expensive computed field recomputation during bulk operations +- Add `refresh_beneficiary_counts()` on program and `refresh_statistics()` on cycle for one-shot recomputation after bulk operations +- Replace `bool(rec.program_membership_ids)` with SQL query in `_compute_has_members` + ### 19.0.2.0.8 - Replace OFFSET pagination with NTILE-based ID-range batching in all async job dispatchers diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html index a11c13ed..a9f08e62 100644 --- a/spp_programs/static/description/index.html +++ b/spp_programs/static/description/index.html @@ -658,6 +658,19 @@