From a44265843076259ab593ec55ed20e61351449fbf Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Sat, 16 May 2026 13:40:40 +0200 Subject: [PATCH] feat: ?pay_type= filter on /workers/ (managers/daily, display-only) Co-Authored-By: Claude Opus 4.7 (1M context) --- core/tests.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ core/views.py | 11 +++++++++++ 2 files changed, 55 insertions(+) diff --git a/core/tests.py b/core/tests.py index a469c6c..7401d97 100644 --- a/core/tests.py +++ b/core/tests.py @@ -3239,6 +3239,50 @@ class WorkerListTeamFilterTests(TestCase): self.assertContains(resp, 'Bravo Team') +class WorkerListPayTypeFilterTests(TestCase): + """The /workers/ page accepts ?pay_type=fixed (managers only) and + ?pay_type=daily (daily workers only). No param = unchanged 'all + pay types' behaviour. Display-only filter — no money math touched.""" + + @classmethod + def setUpTestData(cls): + cls.admin = User.objects.create_user( + username='ptadmin', password='pw', is_staff=True, is_superuser=True, + ) + cls.daily = Worker.objects.create( + name='Danny Daily', id_number='PT-D1', + monthly_salary=Decimal('6000'), # pay_type defaults to 'daily' + ) + cls.mgr = Worker.objects.create( + name='Mary Manager', id_number='PT-M1', + monthly_salary=Decimal('40000'), pay_type='fixed', + ) + + def setUp(self): + self.client.force_login(self.admin) + + def test_pay_type_fixed_shows_only_managers(self): + resp = self.client.get('/workers/?pay_type=fixed') + names = [w.name for w in resp.context['workers']] + self.assertIn('Mary Manager', names) + self.assertNotIn('Danny Daily', names) + self.assertEqual(resp.context['pay_type_filter'], 'fixed') + + def test_pay_type_daily_shows_only_daily(self): + resp = self.client.get('/workers/?pay_type=daily') + names = [w.name for w in resp.context['workers']] + self.assertIn('Danny Daily', names) + self.assertNotIn('Mary Manager', names) + + def test_no_pay_type_param_shows_both(self): + # Regression: the default /workers/ behaviour must NOT change. + resp = self.client.get('/workers/') + names = [w.name for w in resp.context['workers']] + self.assertIn('Danny Daily', names) + self.assertIn('Mary Manager', names) + self.assertEqual(resp.context['pay_type_filter'], '') + + class WorkHistoryTeamFilterTests(TestCase): """The /history/ page accepts ?team= to narrow to logs tagged with that team, ?team=none for logs with no team set, and empty diff --git a/core/views.py b/core/views.py index 7f97aef..206aff6 100644 --- a/core/views.py +++ b/core/views.py @@ -1620,6 +1620,7 @@ def worker_list(request): q = (request.GET.get('q') or '').strip() status = request.GET.get('status') or 'active' team_filter = (request.GET.get('team') or '').strip() + pay_type_filter = (request.GET.get('pay_type') or '').strip() workers = Worker.objects.all() if status == 'active': @@ -1642,6 +1643,15 @@ def worker_list(request): elif team_filter.isdigit(): workers = workers.filter(teams__id=int(team_filter)) + # === Pay-type filter === + # Display-only narrowing by Worker.pay_type. 'fixed' = managers / + # salaried staff; 'daily' = normal field workers. Any other value + # (including absent) leaves the list unfiltered — the default view + # is deliberately unchanged. DB value is 'fixed'/'daily' (Path-A; + # the user-facing label is "Managers (Salaried)"). + if pay_type_filter in ('fixed', 'daily'): + workers = workers.filter(pay_type=pay_type_filter) + # Annotate days worked (distinct WorkLog dates) — shown in the table. # `.distinct()` is also needed to avoid duplicate Worker rows when # the team filter joins through the M2M (a worker on multiple teams @@ -1658,6 +1668,7 @@ def worker_list(request): 'q': q, 'status': status, 'team_filter': team_filter, + 'pay_type_filter': pay_type_filter, 'teams_for_filter': teams_for_filter, 'total_count': workers.count(), }