From 8f5232d1fe2b33676199bc7e89ec2fbf2edcb26d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 30 May 2026 19:57:36 +0200 Subject: [PATCH] Fix weekly summary crash on open entries print_summary_weekly, print_summary_weekly_totals and _aggregate summed raw duration_hours, which is None for open (unfinished) entries, raising TypeError: float + NoneType. Skip open entries from the weekly totals, matching to_csv_entries and parser.aggregate. Add regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/timesheets/output.py | 13 +++++++++++-- tests/test_output.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/timesheets/output.py b/src/timesheets/output.py index d56100e..3f59b69 100644 --- a/src/timesheets/output.py +++ b/src/timesheets/output.py @@ -163,7 +163,12 @@ def print_summary_weekly( day_sections: list[tuple[date, list[dict]]], project_map: dict ) -> None: """Print a full summary for each day of the week.""" - week_total = sum(e["duration_hours"] for _, rows in day_sections for e in rows) + week_total = sum( + e["duration_hours"] + for _, rows in day_sections + for e in rows + if e["duration_hours"] is not None + ) for day, rows in day_sections: aggregated = _aggregate(rows) day_total = sum(e["quantity"] for e in aggregated) @@ -221,7 +226,9 @@ def print_summary_weekly_totals( rows_out = [] for day, rows in day_sections: - day_total = sum(e["duration_hours"] for e in rows) + day_total = sum( + e["duration_hours"] for e in rows if e["duration_hours"] is not None + ) week_total += day_total rows_out.append((day.strftime("%A"), day.strftime("%Y-%m-%d"), day_total)) @@ -245,6 +252,8 @@ def _aggregate(rows: list[dict]) -> list[dict]: key_order: list[tuple] = [] totals: dict[tuple, float] = defaultdict(float) for row in rows: + if row["duration_hours"] is None: + continue description = build_description(row["story"], row["note"]) key = (row["project"].strip(), description) if key not in totals: diff --git a/tests/test_output.py b/tests/test_output.py index 0f130da..7c8acbb 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,4 +1,5 @@ import csv +import datetime import io import pytest @@ -6,6 +7,9 @@ import pytest from timesheets.output import ( print_stories, print_summary, + print_summary_weekly, + print_summary_weekly_short, + print_summary_weekly_totals, to_csv_entries, write_csv, write_csv_weekly, @@ -330,3 +334,38 @@ class TestPrintStories: print_stories([]) out = capsys.readouterr().out assert out == "" + + +# --------------------------------------------------------------------------- +# Weekly summaries with open (unfinished) entries +# --------------------------------------------------------------------------- + + +class TestWeeklySummaryOpenEntries: + """An open entry (start present, end absent) has duration_hours=None and + must be skipped from totals, not crash the weekly summary commands.""" + + def _sections(self): + rows = [ + _raw_row("bugs", "ticket 1", "", 1.0), + _raw_row("bugs", "ticket 2", "", None), # open entry + ] + return [(datetime.date(2026, 1, 6), rows)] + + def test_weekly_totals_skips_open_entry(self, capsys): + print_summary_weekly_totals(self._sections(), {}) + out = capsys.readouterr().out + # only the 1.0h closed entry counts + assert "01:00" in out + assert "WEEK TOTAL" in out + + def test_weekly_full_skips_open_entry(self, capsys): + print_summary_weekly(self._sections(), {}) + out = capsys.readouterr().out + assert "WEEK TOTAL" in out + assert "01:00" in out + + def test_weekly_short_skips_open_entry(self, capsys): + print_summary_weekly_short(self._sections(), {}) + out = capsys.readouterr().out + assert "01:00" in out