Extract _compute_outstanding helper from index() (refactor)
Pure refactor: the ~45 lines of outstanding-payment math inside index() (computing unpaid_wages + pending_adj_add - pending_adj_sub, with a per-project breakdown) move into a standalone _compute_outstanding() helper. index() now calls it with no arguments for unchanged behaviour. The helper accepts optional project_ids / team_ids for Task 3. No tests changed; 28/28 still pass. Dashboard Outstanding Payments card shows the same value before and after. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2eb889a29
commit
6be6a09056
133
core/views.py
133
core/views.py
@ -140,6 +140,82 @@ def get_pay_period(team, reference_date=None):
|
||||
return (None, None)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === OUTSTANDING PAYMENTS — SHARED HELPER ===
|
||||
# Used by the home dashboard AND the payroll report. Computes:
|
||||
# - outstanding_payments: Decimal total (unpaid wages + net unpaid adjustments)
|
||||
# - unpaid_wages: Decimal (pure daily rates for unpaid workers)
|
||||
# - pending_adj_add: Decimal (unpaid additive adjustments, e.g. bonuses)
|
||||
# - pending_adj_sub: Decimal (unpaid deductive adjustments, e.g. loan repayments)
|
||||
# - outstanding_by_project: dict[str project_name -> Decimal amount]
|
||||
#
|
||||
# Accepts optional project_ids / team_ids filters. Empty list or None = no filter.
|
||||
# =============================================================================
|
||||
|
||||
def _compute_outstanding(project_ids=None, team_ids=None):
|
||||
"""Return current-moment outstanding payment breakdown.
|
||||
|
||||
Plain-English: for each work log that hasn't been fully paid, adds up
|
||||
each unpaid worker's daily rate. Then adds unpaid additive adjustments
|
||||
(bonuses, overtime, new loans, advances) and subtracts unpaid deductive
|
||||
adjustments (deductions, loan/advance repayments). Results are the
|
||||
"as of right now" snapshot shown on the home dashboard's Outstanding
|
||||
Payments card. Optional filters scope the answer to specific projects
|
||||
and/or teams.
|
||||
"""
|
||||
# --- Work logs in scope ---
|
||||
work_logs = WorkLog.objects.select_related('project').prefetch_related('workers', 'payroll_records')
|
||||
if project_ids:
|
||||
work_logs = work_logs.filter(project_id__in=project_ids)
|
||||
if team_ids:
|
||||
work_logs = work_logs.filter(team_id__in=team_ids)
|
||||
|
||||
unpaid_wages = Decimal('0.00')
|
||||
outstanding_by_project = {}
|
||||
|
||||
for wl in work_logs:
|
||||
paid_worker_ids = {pr.worker_id for pr in wl.payroll_records.all()}
|
||||
project_name = wl.project.name if wl.project else 'No Project'
|
||||
for worker in wl.workers.all():
|
||||
if worker.id not in paid_worker_ids:
|
||||
cost = worker.daily_rate
|
||||
unpaid_wages += cost
|
||||
outstanding_by_project.setdefault(project_name, Decimal('0.00'))
|
||||
outstanding_by_project[project_name] += cost
|
||||
|
||||
# --- Unpaid adjustments in scope ---
|
||||
adj_qs = PayrollAdjustment.objects.filter(payroll_record__isnull=True).select_related('project')
|
||||
if project_ids:
|
||||
adj_qs = adj_qs.filter(project_id__in=project_ids)
|
||||
if team_ids:
|
||||
# worker__teams is M2M — use subquery pattern (see CLAUDE.md Django ORM gotcha)
|
||||
adj_qs = adj_qs.filter(
|
||||
worker__in=Worker.objects.filter(teams__id__in=team_ids).values('id')
|
||||
)
|
||||
|
||||
pending_adj_add = Decimal('0.00')
|
||||
pending_adj_sub = Decimal('0.00')
|
||||
for adj in adj_qs:
|
||||
project_name = adj.project.name if adj.project else 'No Project'
|
||||
outstanding_by_project.setdefault(project_name, Decimal('0.00'))
|
||||
if adj.type in ADDITIVE_TYPES:
|
||||
pending_adj_add += adj.amount
|
||||
outstanding_by_project[project_name] += adj.amount
|
||||
elif adj.type in DEDUCTIVE_TYPES:
|
||||
pending_adj_sub += adj.amount
|
||||
outstanding_by_project[project_name] -= adj.amount
|
||||
|
||||
outstanding_payments = unpaid_wages + pending_adj_add - pending_adj_sub
|
||||
|
||||
return {
|
||||
'outstanding_payments': outstanding_payments,
|
||||
'unpaid_wages': unpaid_wages,
|
||||
'pending_adj_add': pending_adj_add,
|
||||
'pending_adj_sub': pending_adj_sub,
|
||||
'outstanding_by_project': outstanding_by_project,
|
||||
}
|
||||
|
||||
|
||||
# === HOME DASHBOARD ===
|
||||
# The main page users see after logging in. Shows different content
|
||||
# depending on whether the user is an admin or supervisor.
|
||||
@ -151,56 +227,15 @@ def index(request):
|
||||
if is_admin(user):
|
||||
# --- ADMIN DASHBOARD ---
|
||||
|
||||
# Calculate total value of unpaid work and break it down by project.
|
||||
# A WorkLog is "unpaid for worker X" if no PayrollRecord links BOTH
|
||||
# that log AND that worker. This handles partially-paid logs where
|
||||
# some workers have been paid but others haven't.
|
||||
all_worklogs = WorkLog.objects.select_related(
|
||||
'project'
|
||||
).prefetch_related('workers', 'payroll_records')
|
||||
|
||||
# === OUTSTANDING BREAKDOWN ===
|
||||
# Track unpaid wages and adjustments separately so the dashboard
|
||||
# can show a clear breakdown of what makes up the total.
|
||||
unpaid_wages = Decimal('0.00') # Pure daily rates for unpaid workers
|
||||
pending_adjustments_add = Decimal('0.00') # Unpaid additive adjustments (bonuses, overtime, etc.)
|
||||
pending_adjustments_sub = Decimal('0.00') # Unpaid deductive adjustments (deductions, repayments)
|
||||
outstanding_by_project = {}
|
||||
|
||||
for wl in all_worklogs:
|
||||
# Get the set of worker IDs that have been paid for this log
|
||||
paid_worker_ids = {pr.worker_id for pr in wl.payroll_records.all()}
|
||||
project_name = wl.project.name
|
||||
|
||||
for worker in wl.workers.all():
|
||||
if worker.id not in paid_worker_ids:
|
||||
cost = worker.daily_rate
|
||||
unpaid_wages += cost
|
||||
if project_name not in outstanding_by_project:
|
||||
outstanding_by_project[project_name] = Decimal('0.00')
|
||||
outstanding_by_project[project_name] += cost
|
||||
|
||||
# Also include unpaid payroll adjustments (bonuses, deductions, etc.)
|
||||
# Additive types (Bonus, Overtime, New Loan) increase outstanding.
|
||||
# Deductive types (Deduction, Loan Repayment, Advance Repayment) decrease it.
|
||||
unpaid_adjustments = PayrollAdjustment.objects.filter(
|
||||
payroll_record__isnull=True
|
||||
).select_related('project')
|
||||
|
||||
for adj in unpaid_adjustments:
|
||||
project_name = adj.project.name if adj.project else 'No Project'
|
||||
if project_name not in outstanding_by_project:
|
||||
outstanding_by_project[project_name] = Decimal('0.00')
|
||||
|
||||
if adj.type in ADDITIVE_TYPES:
|
||||
pending_adjustments_add += adj.amount
|
||||
outstanding_by_project[project_name] += adj.amount
|
||||
elif adj.type in DEDUCTIVE_TYPES:
|
||||
pending_adjustments_sub += adj.amount
|
||||
outstanding_by_project[project_name] -= adj.amount
|
||||
|
||||
# Net total = wages + additions - deductions (same result as before, just tracked separately)
|
||||
outstanding_payments = unpaid_wages + pending_adjustments_add - pending_adjustments_sub
|
||||
# Uses the shared _compute_outstanding helper so the dashboard and the
|
||||
# payroll report can't drift. Unscoped (no filters) = whole company.
|
||||
_o = _compute_outstanding()
|
||||
outstanding_payments = _o['outstanding_payments']
|
||||
unpaid_wages = _o['unpaid_wages']
|
||||
pending_adjustments_add = _o['pending_adj_add']
|
||||
pending_adjustments_sub = _o['pending_adj_sub']
|
||||
outstanding_by_project = _o['outstanding_by_project']
|
||||
|
||||
# Sum total paid out in the last 60 days
|
||||
sixty_days_ago = timezone.now().date() - timezone.timedelta(days=60)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user