From ea1e4bdbcbf10c8d9b68ee8adba2dd12cfa10119 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 22 Apr 2026 22:45:08 +0200 Subject: [PATCH] Enrich alltime_projects context with working_days + avg_per_working_day Chapter I of the executive report needs per-project working-day count and avg rand per working day. Instead of modifying the shared _get_labour_costs helper (used by other sections with different column sets), enrich the output INSIDE _build_report_context: wrap the raw result and add working_days (distinct work-log dates per project) and avg_per_working_day (total_cost / working_days, null-safe). Also attaches start_date from the Project model (may be None if not set). 1 test added. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/tests.py | 24 ++++++++++++++++++++++++ core/views.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/core/tests.py b/core/tests.py index 1b734cb..a73222b 100644 --- a/core/tests.py +++ b/core/tests.py @@ -668,3 +668,27 @@ class TeamProjectActivityTests(TestCase): r = _team_project_activity(self.logs_qs) team_names = [row['team_name'] for row in r['rows']] self.assertNotIn('GhostTeam', team_names) + + +class ChapterOneEnrichmentTests(TestCase): + """Chapter I — All Time Projects gains working_days and avg_per_working_day.""" + + def test_alltime_projects_includes_working_days_and_avg(self): + from core.views import _build_report_context + admin = User.objects.create_user(username='c1', is_staff=True) + proj = Project.objects.create(name='C1', start_date=datetime.date(2026, 1, 1)) + w = Worker.objects.create(name='W', id_number='W1', monthly_salary=Decimal('4000')) + # 4 distinct dates, 1 worker each; daily_rate=200; total = R 800; working_days=4; avg=200 + for d in (1, 2, 3, 4): + log = WorkLog.objects.create( + date=datetime.date(2026, 3, d), project=proj, supervisor=admin, + ) + log.workers.add(w) + ctx = _build_report_context( + datetime.date(2026, 1, 1), datetime.date(2026, 12, 31), + ) + by_name = {p['project']: p for p in ctx['alltime_projects']} + self.assertIn('C1', by_name) + self.assertEqual(by_name['C1']['working_days'], 4) + self.assertEqual(by_name['C1']['avg_per_working_day'], Decimal('200.00')) + self.assertEqual(by_name['C1']['start_date'], datetime.date(2026, 1, 1)) diff --git a/core/views.py b/core/views.py index a21bc3b..18fdbf0 100644 --- a/core/views.py +++ b/core/views.py @@ -2114,7 +2114,36 @@ def _build_report_context(start_date, end_date, project_id=None, team_id=None): all_time_logs = all_time_logs.filter(project_id=project_id) if team_id: all_time_logs = all_time_logs.filter(team_id=team_id) - alltime_projects = _get_labour_costs(all_time_logs, 'project__name', 'project') + # === CHAPTER I — All Time Projects (enriched) === + # Adds working_days and avg_per_working_day (the 2026-04-23 design). + # Can't just extend _get_labour_costs because that helper is used by + # other sections with different columns. Wrap it here instead. + alltime_projects_raw = _get_labour_costs(all_time_logs, 'project__name', 'project') + # Build a lookup of working_days per project (distinct work-log dates) + project_working_days = dict( + all_time_logs.filter(project__isnull=False) + .values('project_id', 'project__name') + .annotate(days=Count('date', distinct=True)) + .values_list('project__name', 'days') + ) + # Lookup project start_date from the Project model (authoritative source) + start_dates = dict( + Project.objects.values_list('name', 'start_date') + ) + alltime_projects = [] + for row in alltime_projects_raw: + name = row['project'] + wdays = project_working_days.get(name, 0) + total = row['total'] or Decimal('0.00') + avg = (total / wdays).quantize(Decimal('0.01')) if wdays else Decimal('0.00') + alltime_projects.append({ + 'project': name, + 'worker_days': row['worker_days'], + 'total': total, + 'start_date': start_dates.get(name), # may be None + 'working_days': wdays, + 'avg_per_working_day': avg, + }) alltime_teams = _get_labour_costs( all_time_logs.filter(team__isnull=False), 'team__name', 'team' )