Pure-function helper that classifies each worker on a work log as Paid / Priced-not-paid / Unpaid, collects log-linked adjustments, and computes totals + pay-period context. Used by both the AJAX endpoint and the full-page view so they can't drift. Bootstraps core/tests.py (was empty); 8 tests cover the three statuses, totals, log-linked adjustments, and the pay-period branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
4.7 KiB
Python
112 lines
4.7 KiB
Python
# === TESTS FOR WORK LOG PAYROLL CROSS-LINK ===
|
|
# Covers the _build_work_log_payroll_context helper — the core logic that
|
|
# determines, for each worker on a log, whether they were paid for it.
|
|
|
|
import datetime
|
|
from decimal import Decimal
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
|
|
from core.models import Project, Team, Worker, WorkLog, PayrollRecord, PayrollAdjustment
|
|
from core.views import _build_work_log_payroll_context
|
|
|
|
|
|
class WorkLogPayrollContextTests(TestCase):
|
|
"""Tests for the helper that builds the payroll-status view of a work log."""
|
|
|
|
def setUp(self):
|
|
# Minimal scenario: 1 admin, 1 project, 1 team, 3 workers, 1 log.
|
|
# Worker A has been paid for the log; Worker B is priced-not-paid;
|
|
# Worker C is unpaid.
|
|
self.admin = User.objects.create_user(username='admin', is_staff=True)
|
|
|
|
self.project = Project.objects.create(name='Test Project')
|
|
self.team = Team.objects.create(name='Team X', supervisor=self.admin)
|
|
|
|
self.worker_a = Worker.objects.create(name='Alice', id_number='A1', monthly_salary=Decimal('4000'))
|
|
self.worker_b = Worker.objects.create(name='Bob', id_number='B1', monthly_salary=Decimal('4000'))
|
|
self.worker_c = Worker.objects.create(name='Carol', id_number='C1', monthly_salary=Decimal('4000'))
|
|
|
|
self.log = WorkLog.objects.create(
|
|
date=datetime.date(2026, 4, 10),
|
|
project=self.project,
|
|
team=self.team,
|
|
supervisor=self.admin,
|
|
)
|
|
self.log.workers.add(self.worker_a, self.worker_b, self.worker_c)
|
|
|
|
# Worker A has a PayrollRecord linking them and this log — "Paid".
|
|
self.record_a = PayrollRecord.objects.create(
|
|
worker=self.worker_a,
|
|
amount_paid=Decimal('200.00'),
|
|
date=datetime.date(2026, 4, 15),
|
|
)
|
|
self.record_a.work_logs.add(self.log)
|
|
|
|
# Worker B appears in priced_workers but has no PayrollRecord — "Priced, not paid".
|
|
self.log.priced_workers.add(self.worker_b)
|
|
|
|
# Worker C has neither — "Unpaid".
|
|
|
|
def test_returns_log_and_worker_rows(self):
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
self.assertEqual(ctx['log'], self.log)
|
|
self.assertEqual(len(ctx['worker_rows']), 3)
|
|
|
|
def test_paid_worker_has_payslip_link(self):
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
row = next(r for r in ctx['worker_rows'] if r['worker'].id == self.worker_a.id)
|
|
self.assertEqual(row['status'], 'Paid')
|
|
self.assertEqual(row['payroll_record'], self.record_a)
|
|
self.assertGreater(row['earned'], 0)
|
|
|
|
def test_priced_but_unpaid_worker(self):
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
row = next(r for r in ctx['worker_rows'] if r['worker'].id == self.worker_b.id)
|
|
self.assertEqual(row['status'], 'Priced, not paid')
|
|
self.assertIsNone(row['payroll_record'])
|
|
|
|
def test_totally_unpaid_worker(self):
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
row = next(r for r in ctx['worker_rows'] if r['worker'].id == self.worker_c.id)
|
|
self.assertEqual(row['status'], 'Unpaid')
|
|
self.assertIsNone(row['payroll_record'])
|
|
|
|
def test_totals(self):
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
# Paid = Alice's daily_rate (one record exists for this log+worker).
|
|
self.assertEqual(ctx['total_paid'], self.worker_a.daily_rate)
|
|
# Outstanding = Bob + Carol each at their daily_rate.
|
|
expected = self.worker_b.daily_rate + self.worker_c.daily_rate
|
|
self.assertEqual(ctx['total_outstanding'], expected)
|
|
|
|
def test_adjustments_linked_to_log(self):
|
|
adj = PayrollAdjustment.objects.create(
|
|
worker=self.worker_a,
|
|
project=self.project,
|
|
type='Overtime',
|
|
amount=Decimal('50.00'),
|
|
date=datetime.date(2026, 4, 10),
|
|
description='Extra hour',
|
|
work_log=self.log,
|
|
)
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
self.assertIn(adj, ctx['adjustments'])
|
|
|
|
def test_pay_period_absent_if_no_schedule(self):
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
self.assertEqual(ctx['pay_period'], (None, None))
|
|
|
|
def test_pay_period_present_when_schedule_configured(self):
|
|
self.team.pay_frequency = 'weekly'
|
|
self.team.pay_start_date = datetime.date(2026, 1, 5) # A Monday
|
|
self.team.save()
|
|
ctx = _build_work_log_payroll_context(self.log)
|
|
start, end = ctx['pay_period']
|
|
self.assertIsNotNone(start)
|
|
self.assertIsNotNone(end)
|
|
self.assertLessEqual(start, self.log.date)
|
|
self.assertGreaterEqual(end, self.log.date)
|