diff --git a/core/tests.py b/core/tests.py index 11da607..d2006f0 100644 --- a/core/tests.py +++ b/core/tests.py @@ -534,3 +534,58 @@ class CompanyCostVelocityTests(TestCase): log.workers.add(w1, w2) result = _company_cost_velocity() self.assertEqual(result['working_days'], 1) # not 2 + + +class CurrentOutstandingInScopeTests(TestCase): + """Hero card 2 — 'Outstanding NOW' with optional filter scope.""" + + def setUp(self): + self.admin = User.objects.create_user(username='a-out', is_staff=True) + self.p1 = Project.objects.create(name='ProjA') + self.p2 = Project.objects.create(name='ProjB') + self.t1 = Team.objects.create(name='TeamA', supervisor=self.admin) + self.w = Worker.objects.create( + name='Wkr', id_number='W1', monthly_salary=Decimal('4000') + ) + self.t1.workers.add(self.w) + # Unpaid log on project 1 + log1 = WorkLog.objects.create( + date=datetime.date(2026, 3, 1), + project=self.p1, team=self.t1, supervisor=self.admin, + ) + log1.workers.add(self.w) + # Unpaid log on project 2 + log2 = WorkLog.objects.create( + date=datetime.date(2026, 3, 2), + project=self.p2, team=self.t1, supervisor=self.admin, + ) + log2.workers.add(self.w) + + def test_no_filters_includes_all_projects(self): + from core.views import _current_outstanding_in_scope + result = _current_outstanding_in_scope() + # daily_rate = 4000/20 = 200; 2 unpaid logs * 200 = 400 + self.assertEqual(result['total'], Decimal('400.00')) + self.assertEqual(len(result['by_project']), 2) + + def test_project_filter_scopes_total(self): + from core.views import _current_outstanding_in_scope + result = _current_outstanding_in_scope(project_ids=[self.p1.id]) + self.assertEqual(result['total'], Decimal('200.00')) + self.assertEqual(len(result['by_project']), 1) + self.assertEqual(result['by_project'][0]['name'], 'ProjA') + + def test_team_filter_scopes_total(self): + """Team filter on work logs + worker__teams on adjustments.""" + from core.views import _current_outstanding_in_scope + # Adjustment on a worker not in t1 + other_worker = Worker.objects.create( + name='Other', id_number='O1', monthly_salary=Decimal('4000') + ) + PayrollAdjustment.objects.create( + worker=other_worker, project=self.p1, type='Bonus', + amount=Decimal('500.00'), date=datetime.date(2026, 3, 3), + ) + # With team filter, only self.w's logs appear — R 400 total + result = _current_outstanding_in_scope(team_ids=[self.t1.id]) + self.assertEqual(result['total'], Decimal('400.00')) diff --git a/core/views.py b/core/views.py index 064e9fc..565c0d5 100644 --- a/core/views.py +++ b/core/views.py @@ -252,6 +252,32 @@ def _company_cost_velocity(): } +# ============================================================================= +# === CURRENT OUTSTANDING — SCOPED FOR THE REPORT === +# Thin wrapper around _compute_outstanding that shapes the output for +# the executive report's hero card 2. Includes a 'by_project' list +# sorted by amount desc, ready for direct template rendering. +# ============================================================================= + +def _current_outstanding_in_scope(project_ids=None, team_ids=None): + """Return current outstanding payments, optionally scoped by project/team. + + Calls _compute_outstanding and reshapes the by_project dict into a + list sorted by amount descending (for display). The 'total' field + is the net outstanding (unpaid wages + additive adjustments minus + deductive adjustments), matching the home dashboard card. + """ + raw = _compute_outstanding(project_ids=project_ids, team_ids=team_ids) + by_project_list = sorted( + [{'name': name, 'amount': amt} for name, amt in raw['outstanding_by_project'].items()], + key=lambda r: -r['amount'], + ) + return { + 'total': raw['outstanding_payments'], + 'by_project': by_project_list, + } + + # === HOME DASHBOARD === # The main page users see after logging in. Shows different content # depending on whether the user is an admin or supervisor.