fix: close inline team-map manager-exclusion gap + add cost-rate exclusion test

This commit is contained in:
Konrad du Plessis 2026-05-15 19:46:34 +02:00
parent 65df9f817e
commit 0f45d64eea
2 changed files with 78 additions and 8 deletions

View File

@ -3432,3 +3432,71 @@ class ManagerSalariedAttendanceExclusionTests(TestCase):
ids = m.get(self.team.id) or m.get(str(self.team.id)) or []
self.assertIn(self.daily.id, ids)
self.assertNotIn(self.mgr.id, ids)
# === Point 4: attendance GET cost-estimate map (worker_rates) ===
# The admin attendance form renders a {worker_id: daily_rate} map for
# the live cost estimator. A fixed-salary manager must NOT appear in it
# (they never go on a WorkLog, so they have no daily attendance cost).
# This is a regression GUARD — point 4 was already fixed in 65df9f8, so
# this test is expected to pass immediately; it locks the behaviour in.
def test_attendance_cost_rates_exclude_fixed(self):
self.client.force_login(self.admin)
resp = self.client.get(reverse('attendance_log'))
self.assertEqual(resp.status_code, 200)
# Assert on the actual context map (precise — not a whole-page string
# scan that could collide with the id appearing elsewhere).
worker_rates = resp.context['worker_rates_json']
self.assertIn(str(self.daily.id), worker_rates)
self.assertNotIn(str(self.mgr.id), worker_rates)
# === Fix 1: the two inline tw_map re-render branches ===
# attendance_log POST has two error/re-render branches that used to build
# the team->workers map INLINE without excluding pay_type='fixed'. POST a
# form that is VALID but yields zero loggable dates (single Saturday with
# the Saturday box unchecked) to hit the "no valid dates" re-render branch,
# then assert the rendered team_workers_json excludes the manager.
def test_no_valid_dates_rerender_team_map_excludes_fixed(self):
import json as _json
self.client.force_login(self.admin)
project = Project.objects.create(name='MSA Proj')
# 2026-05-16 is a Saturday. With include_saturday unchecked and no
# end_date, dates_to_log is empty -> "no valid dates" re-render.
resp = self.client.post(reverse('attendance_log'), {
'date': '2026-05-16',
'project': project.id,
'team': self.team.id,
'workers': [self.daily.id],
'overtime_amount': '0.00',
})
self.assertEqual(resp.status_code, 200)
tw_map = _json.loads(resp.context['team_workers_json'])
ids = tw_map.get(str(self.team.id)) or tw_map.get(self.team.id) or []
self.assertIn(self.daily.id, ids)
self.assertNotIn(self.mgr.id, ids)
# === Fix 1: the conflict-warning re-render branch ===
# The second inline tw_map builder lives in the conflict-warning branch.
# Pre-create a WorkLog so the same worker+date collides, forcing the
# conflict re-render, then assert the manager is excluded there too.
def test_conflict_rerender_team_map_excludes_fixed(self):
import json as _json
self.client.force_login(self.admin)
project = Project.objects.create(name='MSA Proj 2')
# 2026-05-18 is a Monday (a valid loggable weekday).
existing = WorkLog.objects.create(
date=datetime.date(2026, 5, 18), project=project)
existing.workers.add(self.daily)
resp = self.client.post(reverse('attendance_log'), {
'date': '2026-05-18',
'project': project.id,
'team': self.team.id,
'workers': [self.daily.id],
'overtime_amount': '0.00',
})
self.assertEqual(resp.status_code, 200)
# Confirm we actually hit the conflict branch (it sets `conflicts`).
self.assertTrue(resp.context.get('conflicts'))
tw_map = _json.loads(resp.context['team_workers_json'])
ids = tw_map.get(str(self.team.id)) or tw_map.get(self.team.id) or []
self.assertIn(self.daily.id, ids)
self.assertNotIn(self.mgr.id, ids)

View File

@ -637,10 +637,11 @@ def attendance_log(request):
if not dates_to_log:
messages.warning(request, 'No valid dates in the selected range.')
# Still need team_workers_json for the JS even on error re-render
tw_map = {}
for t in Team.objects.filter(active=True).prefetch_related('workers'):
tw_map[t.id] = list(t.workers.filter(active=True).values_list('id', flat=True))
# Still need team_workers_json for the JS even on error
# re-render. Use the shared helper so this branch applies the
# SAME admin/supervisor scoping AND manager (pay_type='fixed')
# exclusion as the GET path — no duplicated, drift-prone loop.
tw_map = _build_team_workers_map(user)
return render(request, 'core/attendance_log.html', {
'form': form,
'is_admin': is_admin(user),
@ -669,10 +670,11 @@ def attendance_log(request):
conflict_action = request.POST.get('conflict_action', '')
if conflicts and not conflict_action:
# Show the conflict warning — let user choose Skip or Overwrite
# Still need team_workers_json for the JS even on conflict re-render
tw_map = {}
for t in Team.objects.filter(active=True).prefetch_related('workers'):
tw_map[t.id] = list(t.workers.filter(active=True).values_list('id', flat=True))
# Still need team_workers_json for the JS even on conflict
# re-render. Use the shared helper so this branch applies the
# SAME admin/supervisor scoping AND manager (pay_type='fixed')
# exclusion as the GET path — no duplicated, drift-prone loop.
tw_map = _build_team_workers_map(user)
# Pass the selected worker IDs explicitly for the conflict
# re-submission forms. We can't use form.data.workers in the