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
|
pending_adj_sub_total += worker_adj_sub
|
||||||
|
|
||||||
# --- Payment history ---
|
# --- 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(
|
paid_records = PayrollRecord.objects.select_related(
|
||||||
'worker'
|
'worker'
|
||||||
).order_by('-date', '-id')
|
).prefetch_related('work_logs', 'adjustments').order_by('-date', '-id')
|
||||||
|
|
||||||
# --- Recent payments total (last 60 days, inclusive) ---
|
# --- Recent payments total (last 60 days, inclusive) ---
|
||||||
# 60-day window math: subtract 59 days, not 60. With `>=` that yields
|
# 60-day window math: subtract 59 days, not 60. With `>=` that yields
|
||||||
@ -3952,8 +3956,22 @@ def batch_pay_preview(request):
|
|||||||
),
|
),
|
||||||
).order_by('name')
|
).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:
|
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 ---
|
# --- In 'schedule' mode, skip workers without a pay schedule ---
|
||||||
if mode == 'schedule':
|
if mode == 'schedule':
|
||||||
@ -3966,7 +3984,9 @@ def batch_pay_preview(request):
|
|||||||
has_unpaid = True
|
has_unpaid = True
|
||||||
break
|
break
|
||||||
if not has_unpaid:
|
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:
|
if has_unpaid:
|
||||||
skipped.append({
|
skipped.append({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user