diff --git a/core/tests.py b/core/tests.py
index 9627f4f..ddfd5ad 100644
--- a/core/tests.py
+++ b/core/tests.py
@@ -2571,3 +2571,142 @@ class AbsenceProjectTests(TestCase):
self.assertIsNotNone(a.payroll_adjustment)
self.assertEqual(a.payroll_adjustment.project, self.project)
self.assertEqual(a.payroll_adjustment.type, 'Bonus')
+
+
+# ====================================================================
+# === Worker Absence — Round C: Attendance → Absence shortcut =========
+# Konrad's ask: after submitting attendance, give us a button that
+# jumps straight to /absences/log/ pre-filled with the same date /
+# team / project. The attendance form has two submit buttons
+# differentiated by name=next_action value=log_only|log_absences.
+# ====================================================================
+
+
+class AbsenceAttendanceShortcutTests(TestCase):
+ """Round C — Submit + Log Absences button on /attendance/log/.
+
+ Submitting attendance with next_action='log_absences' redirects to
+ /absences/log/ pre-filled with the date / team / project. The
+ default submit ('log_only' or absent) keeps the existing Site
+ Report flow."""
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.admin = User.objects.create_user(
+ username='roundc-admin', password='pw', is_staff=True,
+ )
+ cls.worker = Worker.objects.create(
+ name='W', id_number='RC1', monthly_salary=Decimal('6000'),
+ )
+ cls.project = Project.objects.create(name='Solar Farm Alpha')
+ cls.team = Team.objects.create(name='Team A', supervisor=cls.admin)
+ cls.team.workers.add(cls.worker)
+
+ def setUp(self):
+ self.client.force_login(self.admin)
+
+ def test_default_attendance_submit_unchanged(self):
+ """next_action absent or 'log_only' → existing Site Report redirect."""
+ resp = self.client.post(reverse('attendance_log'), data={
+ 'date': '2026-05-14',
+ 'project': self.project.id,
+ 'team': self.team.id,
+ 'workers': [self.worker.id],
+ 'overtime_amount': '0.00',
+ 'notes': '',
+ # next_action omitted on purpose — should fall through to
+ # the existing Site Report behaviour.
+ })
+ self.assertEqual(resp.status_code, 302)
+ self.assertIn('/site-report/', resp.url)
+
+ def test_log_only_explicit_value_still_goes_to_site_report(self):
+ """An explicit next_action='log_only' (the default button) keeps
+ the existing behaviour — important for backwards compatibility."""
+ resp = self.client.post(reverse('attendance_log'), data={
+ 'date': '2026-05-14',
+ 'project': self.project.id,
+ 'team': self.team.id,
+ 'workers': [self.worker.id],
+ 'overtime_amount': '0.00',
+ 'notes': '',
+ 'next_action': 'log_only',
+ })
+ self.assertEqual(resp.status_code, 302)
+ self.assertIn('/site-report/', resp.url)
+
+ def test_log_absences_button_redirects_to_absence_log(self):
+ """next_action='log_absences' → redirect to /absences/log/ with
+ date / team / project query-string params pre-filled."""
+ resp = self.client.post(reverse('attendance_log'), data={
+ 'date': '2026-05-14',
+ 'project': self.project.id,
+ 'team': self.team.id,
+ 'workers': [self.worker.id],
+ 'overtime_amount': '0.00',
+ 'notes': '',
+ 'next_action': 'log_absences',
+ })
+ self.assertEqual(resp.status_code, 302)
+ self.assertIn('/absences/log/', resp.url)
+ self.assertIn('date=2026-05-14', resp.url)
+ self.assertIn(f'team={self.team.id}', resp.url)
+ self.assertIn(f'project={self.project.id}', resp.url)
+
+ def test_absence_log_prefills_from_url_params(self):
+ """GET /absences/log/?date=X&team=Y&project=Z → form is
+ pre-populated. We do a cheap rendered-HTML check for the
+ date value (the most user-visible signal)."""
+ url = (
+ reverse('absence_log')
+ + f'?date=2026-05-14&team={self.team.id}&project={self.project.id}'
+ )
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+ # Date field should render with value="2026-05-14"
+ self.assertContains(resp, 'value="2026-05-14"')
+
+ def test_log_absences_intent_survives_conflict_resolution(self):
+ """Round C — when conflicts force a re-render of the attendance form,
+ the next_action='log_absences' value is preserved as a hidden field
+ via the conflict-form's form.data.items loop. Resolving the conflict
+ (e.g. Overwrite) then redirects to /absences/log/ as originally intended."""
+ # Pre-create a conflicting WorkLog for the same worker+date
+ pre_existing_log = WorkLog.objects.create(
+ date=datetime.date(2026, 5, 14),
+ project=self.project,
+ supervisor=self.admin,
+ )
+ pre_existing_log.workers.add(self.worker)
+
+ # First POST — should hit conflict detection (not redirect)
+ resp = self.client.post('/attendance/log/', data={
+ 'date': '2026-05-14',
+ 'project': self.project.id,
+ 'team': self.team.id,
+ 'workers': [self.worker.id],
+ 'overtime_amount': '0.00',
+ 'notes': '',
+ 'next_action': 'log_absences',
+ })
+ # Should render the conflict resolution screen (200), not redirect
+ self.assertEqual(resp.status_code, 200)
+ # The next_action value must be present as a hidden input in the rendered conflict form
+ self.assertContains(resp, 'name="next_action"')
+ self.assertContains(resp, 'value="log_absences"')
+
+ # Second POST — resolve the conflict via Overwrite, carrying next_action through
+ resp2 = self.client.post('/attendance/log/', data={
+ 'date': '2026-05-14',
+ 'project': self.project.id,
+ 'team': self.team.id,
+ 'workers': [self.worker.id],
+ 'overtime_amount': '0.00',
+ 'notes': '',
+ 'next_action': 'log_absences',
+ 'conflict_action': 'overwrite',
+ })
+ # Now should redirect to /absences/log/ with prefill params
+ self.assertEqual(resp2.status_code, 302)
+ self.assertIn('/absences/log/', resp2.url)
+ self.assertIn('date=2026-05-14', resp2.url)
diff --git a/core/views.py b/core/views.py
index a878270..ae9538a 100644
--- a/core/views.py
+++ b/core/views.py
@@ -632,6 +632,31 @@ def attendance_log(request):
else:
messages.warning(request, 'No work logs created — all entries were conflicts.')
+ # === ROUND C: pick post-submit destination ===
+ # The attendance form has TWO submit buttons, both named
+ # `next_action` with different values. Whichever button the
+ # user clicked, that value lands in request.POST:
+ # - 'log_only' (default) → existing Site Report flow
+ # - 'log_absences' → jump to /absences/log/ pre-filled
+ # Konrad's ask: "it is tedious to find the absence form after
+ # logging work — give us a 'Log and add Absence' button."
+ next_action = request.POST.get('next_action', 'log_only')
+
+ if next_action == 'log_absences' and created_log_ids:
+ # Pre-fill the absence form with the same date / team /
+ # project the user just used. We use the LAST log's date
+ # (matches the site_report_edit behaviour for date ranges:
+ # the supervisor lands on the most recent day).
+ # Use the local-scope variables we already have from the
+ # form's cleaned_data — no need to re-fetch the WorkLog.
+ from urllib.parse import urlencode
+ params = {'date': dates_to_log[-1].isoformat()}
+ if team:
+ params['team'] = team.id
+ if project:
+ params['project'] = project.id
+ return redirect(f"{reverse('absence_log')}?{urlencode(params)}")
+
# Two-step flow: after attendance, send the supervisor to the
# site-report form so they can log progress + weather while it's
# fresh in their head. The form has a "Skip" link to home for
@@ -5269,7 +5294,19 @@ def absence_log(request):
messages.success(request, f'{len(form.expanded_pairs())} absence(s) logged.')
return redirect('absence_list')
else:
- form = AbsenceLogForm(user=request.user)
+ # === ROUND C: pre-fill from URL params ===
+ # When the user clicked "Log Work + Add Absences" on the attendance
+ # form, we land here with ?date=...&team=...&project=... in the
+ # query string. Drop those values into form `initial=` so the
+ # fields render pre-populated. Plain GET (no params) → blank form.
+ initial = {}
+ if request.GET.get('date'):
+ initial['date'] = request.GET.get('date')
+ if request.GET.get('team', '').isdigit():
+ initial['team'] = request.GET.get('team')
+ if request.GET.get('project', '').isdigit():
+ initial['project'] = request.GET.get('project')
+ form = AbsenceLogForm(user=request.user, initial=initial)
# === TEAM → WORKERS MAP for the in-page team filter (Fix A1, May 2026) ===
# Mirrors the pattern in attendance_log(): build a dict of team_id →