feat(spp_programs): show cycle compliance status on registrant form#152
feat(spp_programs): show cycle compliance status on registrant form#152
Conversation
Add visibility into cycle-level compliance status from the registrant form. Users can now see at a glance which programs have non-compliant cycles and drill down to the specific cycle details. - Add latest_cycle_state computed field on spp.program.membership showing the most recent cycle state per registrant+program - Add "View Cycles" button on program membership rows opening filtered cycle memberships for that registrant+program - Add compliance_criteria computed field on spp.cycle.membership showing the program CEL expression when non-compliant - Add cycle_membership_count and non_compliant_cycle_count on res.partner with smart button - Add Non-Compliant filter to cycle membership search view Closes #860
There was a problem hiding this comment.
Code Review
This pull request introduces features to track and display compliance information and cycle membership status for registrants. Key additions include a compliance_criteria field on cycle memberships, a latest_cycle_state field on program memberships, and new statistics and action buttons on the registrant view. A performance issue was identified in the _compute_latest_cycle_state method, where an N+1 query pattern should be replaced with a batch query using _read_group to improve efficiency.
| def _compute_latest_cycle_state(self): | ||
| """Get the latest cycle membership state per registrant+program.""" | ||
| if not self: | ||
| return | ||
|
|
||
| for rec in self: | ||
| rec.latest_cycle_state = False | ||
|
|
||
| # Batch query: find latest cycle membership for each program membership | ||
| CycleMembership = self.env["spp.cycle.membership"] | ||
| for rec in self: | ||
| cycle_mem = CycleMembership.search( | ||
| [ | ||
| ("partner_id", "=", rec.partner_id.id), | ||
| ("cycle_id.program_id", "=", rec.program_id.id), | ||
| ], | ||
| order="id desc", | ||
| limit=1, | ||
| ) | ||
| if cycle_mem: | ||
| rec.latest_cycle_state = cycle_mem.state |
There was a problem hiding this comment.
The current implementation of _compute_latest_cycle_state performs a search() inside a loop, which leads to an N+1 query pattern. This will significantly degrade performance when viewing a list of program memberships (e.g., on the registrant form). It is highly recommended to use _read_group to fetch the latest cycle membership IDs for all records in the batch in a single query.
def _compute_latest_cycle_state(self):
"""Get the latest cycle membership state per registrant+program."""
for rec in self:
rec.latest_cycle_state = False
if not self:
return
# Batch query: find latest cycle membership ID for each partner+program pair
data = self.env["spp.cycle.membership"]._read_group(
domain=[
("partner_id", "in", self.partner_id.ids),
("cycle_id.program_id", "in", self.program_id.ids),
],
groupby=["partner_id", "cycle_id.program_id"],
aggregates=["id:max"],
)
# Map (partner_id, program_id) -> latest_membership_id
latest_ids = {(p.id, prog.id): max_id for p, prog, max_id in data}
# Fetch states for all latest memberships in one query
mems = self.env["spp.cycle.membership"].browse(latest_ids.values())
mem_states = {m.id: m.state for m in mems}
for rec in self:
mem_id = latest_ids.get((rec.partner_id.id, rec.program_id.id))
if mem_id:
rec.latest_cycle_state = mem_states.get(mem_id)
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 19.0 #152 +/- ##
==========================================
+ Coverage 71.45% 71.46% +0.01%
==========================================
Files 932 932
Lines 54792 54846 +54
==========================================
+ Hits 39152 39198 +46
- Misses 15640 15648 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…ycle membership list The compliance view override colored non_compliant as decoration-warning (yellow), same as paused. Changed to decoration-danger (red) to match the registrant form and eliminate ambiguity with paused state.
The single smart button crammed both cycle count and non-compliant count into one button causing misaligned text. Split into two buttons: one for total Cycles count and one for Non-Compliant count (with warning icon, only visible when > 0).
Added Program column (from cycle_id.program_id) so users can identify which program a cycle belongs to. Made Registrant and Program columns optional=show. Added enrollment_date as optional=hide.
Dotted field notation (cycle_id.program_id) does not work in list views. Added a stored related field program_id on spp.cycle.membership and updated the view to use it.
The Non-Compliant smart button used the same action as the Cycles button, showing all cycle memberships. Added action_view_non_compliant_cycles that filters to state=non_compliant only.
…-on-registrant # Conflicts: # spp_programs/tests/__init__.py
Why is this change needed?
When a registrant is marked as non-compliant on a cycle membership, there is no way to see this from the registrant form. Users have to navigate through programs and cycles manually to find compliance issues. This makes it difficult to quickly identify beneficiaries with compliance problems.
How was the change implemented?
Latest Cycle Status column on program membership list
latest_cycle_statecomputed field onspp.program.membershipthat reads the most recent cycle membership state per registrant+programView Cycles button
Compliance Criteria column on cycle membership list
compliance_criteriacomputed field onspp.cycle.membershipper_capita_income < poverty_line)Smart button on registrant form
cycle_membership_countandnon_compliant_cycle_countonres.partnerSearch filter
New unit tests
N/A — existing 579 spp_programs tests all pass (0 failures)
Unit tests executed by the author
How to test manually
Related links