perf: kill per-row queries on the History tab and Batch Pay preview
- paid_records (History tab) now prefetches work_logs + adjustments: the template shows a day-count and loops adjustments per row, which fired 2 queries per visible record (~100 on a long history). - batch_pay_preview replaces the per-worker get_worker_active_team() call (worker.teams.filter(...).first() — bypasses prefetch, 1 query per worker) with the same batched membership-dict pattern payroll_dashboard already uses, and reads the unpaid-adjustments check from the existing filtered prefetch instead of .exists(). Also includes (committed earlier in 25910b2 but noting for the record): the /report/ worker-breakdown loop's per-worker-per-type aggregates were replaced by one GROUP BY dict (audit fix #7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
25910b2861
commit
541b8973c7
@ -3094,9 +3094,13 @@ def payroll_dashboard(request):
|
||||
pending_adj_sub_total += worker_adj_sub
|
||||
|
||||
# --- Payment history ---
|
||||
# prefetch work_logs + adjustments: the History tab template shows
|
||||
# "{{ record.work_logs.count }} days" and loops record.adjustments.all
|
||||
# per row — without the prefetch that's 2 queries per visible record
|
||||
# (audit fix #9, Jun 2026).
|
||||
paid_records = PayrollRecord.objects.select_related(
|
||||
'worker'
|
||||
).order_by('-date', '-id')
|
||||
).prefetch_related('work_logs', 'adjustments').order_by('-date', '-id')
|
||||
|
||||
# --- Recent payments total (last 60 days, inclusive) ---
|
||||
# 60-day window math: subtract 59 days, not 60. With `>=` that yields
|
||||
@ -3952,8 +3956,22 @@ def batch_pay_preview(request):
|
||||
),
|
||||
).order_by('name')
|
||||
|
||||
# === Active team per worker — ONE batched membership query ===
|
||||
# get_worker_active_team() runs worker.teams.filter(...).first() per
|
||||
# worker, which bypasses the prefetch cache and fired a query per row
|
||||
# (audit fix #10, Jun 2026). Same dict pattern as payroll_dashboard.
|
||||
active_team_by_id = {t.id: t for t in Team.objects.filter(active=True)}
|
||||
worker_active_team = {}
|
||||
for membership in Team.workers.through.objects.filter(
|
||||
team_id__in=active_team_by_id.keys()
|
||||
).values('team_id', 'worker_id'):
|
||||
wid = membership['worker_id']
|
||||
if wid in worker_active_team:
|
||||
continue
|
||||
worker_active_team[wid] = active_team_by_id[membership['team_id']]
|
||||
|
||||
for worker in active_workers:
|
||||
team = get_worker_active_team(worker)
|
||||
team = worker_active_team.get(worker.id)
|
||||
|
||||
# --- In 'schedule' mode, skip workers without a pay schedule ---
|
||||
if mode == 'schedule':
|
||||
@ -3966,7 +3984,9 @@ def batch_pay_preview(request):
|
||||
has_unpaid = True
|
||||
break
|
||||
if not has_unpaid:
|
||||
has_unpaid = worker.adjustments.filter(payroll_record__isnull=True).exists()
|
||||
# the adjustments prefetch above is already filtered to
|
||||
# unpaid rows — bool() reads the cache, no extra query
|
||||
has_unpaid = bool(worker.adjustments.all())
|
||||
|
||||
if has_unpaid:
|
||||
skipped.append({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user