diff --git a/core/context.py b/core/context.py index 430552a..713f415 100644 --- a/core/context.py +++ b/core/context.py @@ -5,4 +5,5 @@ def navigation(_request): ("transaction-list", "Transactions"), ("category-list", "Categories"), ("payee-list", "Payees"), + ("monthly-report", "Reports"), )} diff --git a/core/forms.py b/core/forms.py index 4587b2e..2699f3e 100644 --- a/core/forms.py +++ b/core/forms.py @@ -110,6 +110,9 @@ class ReportSelectionForm(forms.Form): if match is None: raise forms.ValidationError("invalid_month", code="invalid_month") try: - return date(int(match["year"]), int(match["month"]), 1) + month = date(int(match["year"]), int(match["month"]), 1) except ValueError as exc: raise forms.ValidationError("invalid_month", code="invalid_month") from exc + if month.year == date.max.year and month.month == 12: + raise forms.ValidationError("invalid_month", code="invalid_month") + return month diff --git a/finance/reports.py b/finance/reports.py index ada2482..050c09a 100644 --- a/finance/reports.py +++ b/finance/reports.py @@ -148,6 +148,8 @@ def _validated_period(month: date, currency: str) -> tuple[date, str, date]: errors["month"] = "invalid_date" elif month.day != 1: errors["month"] = "must_be_first_day" + elif month.year == date.max.year and month.month == 12: + errors["month"] = "invalid_month" if not isinstance(currency, str) or _CURRENCY_RE.fullmatch(currency) is None: errors["currency"] = "invalid_currency" if errors: diff --git a/tests/finance/test_reports.py b/tests/finance/test_reports.py index 7d62427..728987e 100644 --- a/tests/finance/test_reports.py +++ b/tests/finance/test_reports.py @@ -110,6 +110,7 @@ def test_available_currencies_are_distinct_and_sorted(): ("month", "currency", "errors"), [ (date(2026, 1, 2), "BRL", {"month": "must_be_first_day"}), + (date(9999, 12, 1), "BRL", {"month": "invalid_month"}), (date(2026, 1, 1), "brl", {"currency": "invalid_currency"}), ("2026-01", "BRL", {"month": "invalid_date"}), ], diff --git a/tests/test_report_views.py b/tests/test_report_views.py index 1458122..e921869 100644 --- a/tests/test_report_views.py +++ b/tests/test_report_views.py @@ -91,6 +91,23 @@ def test_home_uses_brl_when_no_account_currency_exists(client, monkeypatch): assert '' in response.content.decode() +def test_monthly_report_rejects_month_without_representable_next_month(client): + response = client.get( + reverse("monthly-report"), + {"month": "9999-12", "currency": "BRL"}, + ) + + assert response.status_code == 400 + assert "invalid_month" in response.content.decode() + + +def test_reports_are_linked_from_non_dashboard_pages(client): + response = client.get(reverse("transaction-list")) + + assert response.status_code == 200 + assert f'href="{reverse("monthly-report")}">Reports' in response.content.decode() + + def test_negative_spending_and_balance_keep_their_sign_and_are_labeled(client): bank = make_account() post(bank, "-100.00", Transaction.Kind.OPENING, date(2025, 12, 31))