diff --git a/core/templates/core/report.html b/core/templates/core/report.html index 177f233..b3d0538 100644 --- a/core/templates/core/report.html +++ b/core/templates/core/report.html @@ -199,33 +199,43 @@ {# === HERO KPI BAND === #} +{# Sub-labels intentionally call out current-pay-rate basis (Finding 2) + and the active filter scope (Finding 5) so the KPIs aren't read as + apples-to-apples when filters are on. #}
Paid This Period
R {{ total_paid_out|money }}
-
{{ start_date|date:"d M Y" }} – {{ end_date|date:"d M Y" }}
+
+ {{ start_date|date:"d M Y" }} – {{ end_date|date:"d M Y" }} · includes adjustments +
Outstanding Now
R {{ current_outstanding.total|money }}
-
as of {{ current_as_of|date:"H:i" }}
+
+ as of {{ current_as_of|date:"H:i" }} + {% if project_name != 'All Projects' or team_name != 'All Teams' %} + · scoped to {{ project_name }}{% if team_name != 'All Teams' %} / {{ team_name }}{% endif %} + {% endif %} +
-
FoxFitt Avg / Day
+
Company Avg / Working Day
R {{ company_avg_daily|money }}
-
lifetime avg · {{ company_working_days }} working days
+
lifetime · all crews · at current pay rates
-
FoxFitt Avg / Month
+
Company Avg / Month
R {{ company_avg_monthly|money }}
-
lifetime avg · ~30.44 days/month
+
lifetime · ~30.44 days/month · at current pay rates
@@ -401,7 +411,15 @@ {% if cost_per_project %}
- + + + + + + + {% for item in cost_per_project %} @@ -422,7 +440,15 @@ {% if cost_per_team %}
ProjectWorker-DaysTotal Cost
ProjectWorker-Days + Day-Rate Cost +
{{ item.project }}{{ item.worker_days }}R {{ item.total|money }}
- + + + + + + + {% for item in cost_per_team %} @@ -471,6 +497,14 @@
TeamWorker-DaysTotal Cost
TeamWorker-Days + Day-Rate Cost +
{{ item.team }}{{ item.worker_days }}R {{ item.total|money }}
+ {# Footnote — explains why Days and Total Paid can read as + "not matching" within a single period (different timing). #} +

+ + Days reflect work logged in this period. + Total Paid reflects payments received in this period — + they may not match if a worker was paid in this period for previous-period work. +

{% else %}

No worker payment data for this period.

{% endif %} diff --git a/core/views.py b/core/views.py index 04de15c..cae1f7d 100644 --- a/core/views.py +++ b/core/views.py @@ -3308,18 +3308,25 @@ def payroll_dashboard(request): }) # === CHART DATA: Per-Worker Monthly Breakdown === - # Pre-compute payment breakdown for each active worker over the last 6 months. + # Pre-compute payment breakdown for each worker over the last 6 months. # This powers the "By Worker" toggle on the Monthly Payroll Totals chart. # Only ~14 workers x 6 months = tiny dataset, so we embed it all as JSON # and switching between workers is instant (no server round-trips). # + # SCOPE: no `worker__active=True` filter — historical payments belong + # to the worker they were made to, even if that worker has since been + # deactivated. This matches `recent_payments_total` above (which also + # has no active-filter). The outer loop later iterates `active_workers` + # only, so deactivated-worker rows in the lookup dicts are simply + # ignored — but the SQL stays consistent across all payroll-dashboard + # stats (Finding 18, May 2026). + # # `six_months_ago_date` is already defined above (hoisted next to the # date-window setup) and reused here. # Query 1: Total amount paid per worker per month. # Uses database-level grouping — one query for ALL workers at once. worker_monthly_paid_qs = PayrollRecord.objects.filter( - worker__active=True, date__gte=six_months_ago_date, ).values( 'worker_id', @@ -3338,7 +3345,6 @@ def payroll_dashboard(request): # so it lines up with when the payment actually happened. worker_monthly_adj_qs = PayrollAdjustment.objects.filter( payroll_record__isnull=False, - worker__active=True, payroll_record__date__gte=six_months_ago_date, ).values( 'worker_id',