From 18c75b2bce4cfe2fe564320ff47a07ed58888fc6 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Fri, 15 May 2026 01:46:47 +0200 Subject: [PATCH] fix(dashboard): 'Paid This Month' actually uses calendar month MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard card labeled 'Paid This Month' was summing the last 60 days of PayrollRecords — identical to the payroll dashboard's 'Paid (60D)' card. Misleading at best, wrong at worst when explaining the dashboard to a non-developer. Now filters by date__year + date__month (current calendar month only). Added 3 regression tests: excludes 45-day-old payment, includes 1st-of-month payment, returns 0 cleanly when nothing paid yet this month. Found during Konrad's 15 May audit of dashboard numbers. --- core/tests.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ core/views.py | 12 ++++++++--- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/core/tests.py b/core/tests.py index 5ac8e9e..2969d86 100644 --- a/core/tests.py +++ b/core/tests.py @@ -3008,3 +3008,60 @@ class WorkHistoryTeamFilterTests(TestCase): resp = self.client.get(f'/history/?team={self.team_a.id}') self.assertIn(f'team={self.team_a.id}', resp.context['filter_params']) self.assertEqual(resp.context['selected_team'], str(self.team_a.id)) + + +class DashboardPaidThisMonthTests(TestCase): + """Regression: 'Paid This Month' on the admin dashboard must be the + CURRENT CALENDAR MONTH only — not a rolling 60-day window. Previously + this card showed the same value as the payroll dashboard's 'Paid (60D)' + card, which was misleading when explaining the dashboard to a + non-developer.""" + + @classmethod + def setUpTestData(cls): + cls.admin = User.objects.create_user( + username='admin', password='pw', is_staff=True, is_superuser=True, + ) + cls.worker = Worker.objects.create( + name='Pay W', id_number='PW1', monthly_salary=Decimal('6000'), + ) + + def setUp(self): + self.client.force_login(self.admin) + + def test_paid_this_month_excludes_last_month(self): + """A payment dated 45 days ago (almost certainly in the prior + calendar month) must NOT count toward 'Paid This Month'.""" + today = datetime.date.today() + forty_five_days_ago = today - datetime.timedelta(days=45) + + # In-month payment (counts) + PayrollRecord.objects.create( + worker=self.worker, amount_paid=Decimal('1000.00'), date=today, + ) + # 45-days-old payment (does NOT count — almost always prior month) + PayrollRecord.objects.create( + worker=self.worker, amount_paid=Decimal('9999.00'), + date=forty_five_days_ago, + ) + + resp = self.client.get('/') + self.assertEqual(resp.context['paid_this_month'], Decimal('1000.00')) + + def test_paid_this_month_includes_first_of_month(self): + """A payment dated 1st of the current month counts (boundary case).""" + today = datetime.date.today() + first_of_month = today.replace(day=1) + + PayrollRecord.objects.create( + worker=self.worker, amount_paid=Decimal('500.00'), + date=first_of_month, + ) + + resp = self.client.get('/') + self.assertEqual(resp.context['paid_this_month'], Decimal('500.00')) + + def test_paid_this_month_zero_when_no_payments(self): + """No payments in the current month → zero (not None).""" + resp = self.client.get('/') + self.assertEqual(resp.context['paid_this_month'], Decimal('0.00')) diff --git a/core/views.py b/core/views.py index 12b4cdd..c4b9209 100644 --- a/core/views.py +++ b/core/views.py @@ -372,10 +372,16 @@ def index(request): 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) + # === PAID THIS MONTH (calendar month, 1st → today) === + # The dashboard card is labeled "Paid This Month" and must reflect + # the CURRENT CALENDAR MONTH only — not a rolling 60-day window. + # The payroll dashboard has its own separate "Paid (60D)" card if + # the rolling-window view is wanted. Filtering by date__year + + # date__month is unambiguous and matches the label exactly. + _today_dt = timezone.now().date() paid_this_month = PayrollRecord.objects.filter( - date__gte=sixty_days_ago + date__year=_today_dt.year, + date__month=_today_dt.month, ).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0.00') # Count and total balance of active loans