Add outstanding payments breakdown on dashboard

Split the single outstanding total into unpaid wages, additions, and
deductions so the card shows where the number comes from. Rename the
'General' project bucket to 'No Project' so per-project totals now
visibly sum to the overall total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-03-05 15:56:40 +02:00
parent d51d06d28d
commit d33d5943f9
2 changed files with 43 additions and 7 deletions

View File

@ -24,6 +24,7 @@
<!-- Admin View --> <!-- Admin View -->
<div class="row g-4 mb-4 position-relative"> <div class="row g-4 mb-4 position-relative">
<!-- Outstanding Payments Card --> <!-- Outstanding Payments Card -->
<!-- Shows the total owed to workers, with a breakdown of wages vs adjustments -->
<div class="col-xl-3 col-md-6"> <div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2"> <div class="card stat-card h-100 py-2">
<div class="card-body"> <div class="card-body">
@ -32,8 +33,32 @@
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;"> <div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;">
Outstanding Payments</div> Outstanding Payments</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_payments|floatformat:2 }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">R {{ outstanding_payments|floatformat:2 }}</div>
{# === BREAKDOWN — only shown when there are pending adjustments === #}
{% if pending_adjustments_add or pending_adjustments_sub %}
<div class="mt-2 pt-2 border-top" style="font-size: 0.75rem; color: #64748b;">
<div class="d-flex justify-content-between">
<span>Unpaid wages</span>
<span>R {{ unpaid_wages|floatformat:2 }}</span>
</div>
{% if pending_adjustments_add %}
<div class="d-flex justify-content-between">
<span>+ Additions</span>
<span class="text-success">R {{ pending_adjustments_add|floatformat:2 }}</span>
</div>
{% endif %}
{% if pending_adjustments_sub %}
<div class="d-flex justify-content-between">
<span>- Deductions</span>
<span class="text-danger">-R {{ pending_adjustments_sub|floatformat:2 }}</span>
</div>
{% endif %}
</div>
{% endif %}
<div class="mt-1" style="font-size: 0.65rem; color: #94a3b8;">
<i class="fas fa-info-circle"></i> Loan repayments deducted at payment time
</div>
</div> </div>
<div class="col-auto"> <div class="col-auto align-self-start">
<i class="fas fa-exclamation-circle fa-2x text-danger opacity-50"></i> <i class="fas fa-exclamation-circle fa-2x text-danger opacity-50"></i>
</div> </div>
</div> </div>

View File

@ -79,7 +79,12 @@ def index(request):
'project' 'project'
).prefetch_related('workers', 'payroll_records') ).prefetch_related('workers', 'payroll_records')
outstanding_payments = Decimal('0.00') # === 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 = {} outstanding_by_project = {}
for wl in all_worklogs: for wl in all_worklogs:
@ -90,30 +95,33 @@ def index(request):
for worker in wl.workers.all(): for worker in wl.workers.all():
if worker.id not in paid_worker_ids: if worker.id not in paid_worker_ids:
cost = worker.daily_rate cost = worker.daily_rate
outstanding_payments += cost unpaid_wages += cost
if project_name not in outstanding_by_project: if project_name not in outstanding_by_project:
outstanding_by_project[project_name] = Decimal('0.00') outstanding_by_project[project_name] = Decimal('0.00')
outstanding_by_project[project_name] += cost outstanding_by_project[project_name] += cost
# Also include unpaid payroll adjustments (bonuses, deductions, etc.) # Also include unpaid payroll adjustments (bonuses, deductions, etc.)
# Additive types (Bonus, Overtime, New Loan) increase outstanding. # Additive types (Bonus, Overtime, New Loan) increase outstanding.
# Deductive types (Deduction, Loan Repayment, Advance Payment) decrease it. # Deductive types (Deduction, Loan Repayment, Advance Repayment) decrease it.
unpaid_adjustments = PayrollAdjustment.objects.filter( unpaid_adjustments = PayrollAdjustment.objects.filter(
payroll_record__isnull=True payroll_record__isnull=True
).select_related('project') ).select_related('project')
for adj in unpaid_adjustments: for adj in unpaid_adjustments:
project_name = adj.project.name if adj.project else 'General' project_name = adj.project.name if adj.project else 'No Project'
if project_name not in outstanding_by_project: if project_name not in outstanding_by_project:
outstanding_by_project[project_name] = Decimal('0.00') outstanding_by_project[project_name] = Decimal('0.00')
if adj.type in ADDITIVE_TYPES: if adj.type in ADDITIVE_TYPES:
outstanding_payments += adj.amount pending_adjustments_add += adj.amount
outstanding_by_project[project_name] += adj.amount outstanding_by_project[project_name] += adj.amount
elif adj.type in DEDUCTIVE_TYPES: elif adj.type in DEDUCTIVE_TYPES:
outstanding_payments -= adj.amount pending_adjustments_sub += adj.amount
outstanding_by_project[project_name] -= 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
# Sum total paid out in the last 60 days # Sum total paid out in the last 60 days
sixty_days_ago = timezone.now().date() - timezone.timedelta(days=60) sixty_days_ago = timezone.now().date() - timezone.timedelta(days=60)
paid_this_month = PayrollRecord.objects.filter( paid_this_month = PayrollRecord.objects.filter(
@ -148,6 +156,9 @@ def index(request):
context = { context = {
'is_admin': True, 'is_admin': True,
'outstanding_payments': outstanding_payments, 'outstanding_payments': outstanding_payments,
'unpaid_wages': unpaid_wages,
'pending_adjustments_add': pending_adjustments_add,
'pending_adjustments_sub': pending_adjustments_sub,
'paid_this_month': paid_this_month, 'paid_this_month': paid_this_month,
'active_loans_count': active_loans_count, 'active_loans_count': active_loans_count,
'active_loans_balance': active_loans_balance, 'active_loans_balance': active_loans_balance,