feat(reports): Auswertungen, Kalender, Budgets und Export
Backend: - Gemeinsame Bewegungsschicht flows(), auf der alle Berichte aufbauen; Plan und Ist bleiben dabei getrennt - Forecast, Kategorien mit Drilldown, Abo-Übersicht, Jahresvergleich, Cashflow-Kalender, Budget-Ampel, Sparziel-Fortschritt, gebündeltes Dashboard - Budgetübertrag über Monatsgrenzen, Budgets auf Oberkategorien schließen Unterkategorien ein - Export als CSV (BOM, Semikolon, deutsches Dezimaltrennzeichen) und XLSX mit typisierten Beträgen Frontend: - Dashboard, Cashflow-Kalender mit Bestätigen direkt am Tag, Budget-, Sparziel- und Auswertungsseite - Diagrammpalette gegen beide Flächen auf Kontrast und Farbfehlsichtigkeit geprüft; Grün/Rot als Serienpaar verworfen - Einnahmen/Ausgaben und kumulierter Saldo in getrennten Diagrammen statt auf zwei Größenachsen - Recharts in einen eigenen Chunk ausgelagert 27 neue Backend-Tests (235 gesamt), 16 neue Frontend-Tests (65 gesamt) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -4,7 +4,9 @@ from datetime import date
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from app.models.enums import EntryKind, OccurrenceStatus
|
||||
from app.schemas.common import ApiModel, Money
|
||||
from app.schemas.recurrence import ContractTermOut
|
||||
|
||||
|
||||
class TotalsOut(ApiModel):
|
||||
@@ -43,3 +45,178 @@ class MonthReportOut(ApiModel):
|
||||
confirmed_count: int
|
||||
open_count: int
|
||||
skipped_count: int
|
||||
|
||||
|
||||
class ForecastMonthOut(ApiModel):
|
||||
"""Ein Monat der Vorschau."""
|
||||
|
||||
month: date
|
||||
income: Money
|
||||
expenses: Money
|
||||
balance: Money
|
||||
cumulative_balance: Money = Field(
|
||||
description="Prognostizierter Kontostand am Monatsende über alle Konten."
|
||||
)
|
||||
|
||||
|
||||
class ForecastOut(ApiModel):
|
||||
months: list[ForecastMonthOut]
|
||||
total_income: Money
|
||||
total_expenses: Money
|
||||
|
||||
|
||||
class CategorySliceOut(ApiModel):
|
||||
"""Summe einer Kategorie; Oberkategorien tragen ihre Unterkategorien."""
|
||||
|
||||
category_id: int
|
||||
name: str
|
||||
color: str
|
||||
icon: str
|
||||
amount: Money
|
||||
count: int
|
||||
children: list["CategorySliceOut"] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CategoryReportOut(ApiModel):
|
||||
date_from: date
|
||||
date_to: date
|
||||
kind: EntryKind
|
||||
total: Money
|
||||
categories: list[CategorySliceOut]
|
||||
|
||||
|
||||
class SubscriptionOut(ApiModel):
|
||||
"""Ein laufender Posten mit Jahreskosten."""
|
||||
|
||||
recurrence_id: int
|
||||
title: str
|
||||
merchant_id: int | None
|
||||
merchant_name: str | None
|
||||
category_id: int
|
||||
amount: Money
|
||||
annual_cost: Money
|
||||
monthly_cost: Money
|
||||
rrule: str
|
||||
is_installment: bool = Field(
|
||||
description="Ratenzahlungen zählen nicht in die Summe „Abos gesamt p. a.“."
|
||||
)
|
||||
is_cancelled: bool
|
||||
contract_term: ContractTermOut | None = None
|
||||
days_until_notice: int | None = Field(
|
||||
default=None, description="Tage bis zum letzten Kündigungstermin."
|
||||
)
|
||||
|
||||
|
||||
class SubscriptionReportOut(ApiModel):
|
||||
entries: list[SubscriptionOut]
|
||||
total_annual: Money = Field(description="Summe über alle Posten ohne Ratenzahlungen.")
|
||||
total_monthly: Money
|
||||
upcoming_deadlines: list[SubscriptionOut] = Field(
|
||||
description="Kündigungsfristen, die in den nächsten 60 Tagen ablaufen."
|
||||
)
|
||||
|
||||
|
||||
class YearComparisonRowOut(ApiModel):
|
||||
category_id: int
|
||||
name: str
|
||||
color: str
|
||||
current: Money
|
||||
previous: Money
|
||||
delta: Money
|
||||
|
||||
|
||||
class YearComparisonOut(ApiModel):
|
||||
year: int
|
||||
rows: list[YearComparisonRowOut]
|
||||
current_total: Money
|
||||
previous_total: Money
|
||||
|
||||
|
||||
class CalendarEntryOut(ApiModel):
|
||||
"""Ein fälliger Posten im Kalender."""
|
||||
|
||||
title: str
|
||||
kind: EntryKind
|
||||
amount: Money
|
||||
category_id: int
|
||||
merchant_id: int | None
|
||||
account_id: int | None
|
||||
source: str = Field(description="'recurrence' oder 'transaction'.")
|
||||
recurrence_id: int | None = None
|
||||
occurrence_date: date | None = Field(
|
||||
default=None, description="Nominales Datum – Schlüssel für Bestätigen und Auslassen."
|
||||
)
|
||||
status: OccurrenceStatus | None = None
|
||||
is_variable: bool = False
|
||||
|
||||
|
||||
class CalendarDayOut(ApiModel):
|
||||
date: date
|
||||
entries: list[CalendarEntryOut]
|
||||
net: Money = Field(description="Saldo des Tages, Ausgaben negativ.")
|
||||
running_balance: Money
|
||||
is_business_day: bool
|
||||
|
||||
|
||||
class CalendarMonthOut(ApiModel):
|
||||
month: date
|
||||
days: list[CalendarDayOut]
|
||||
opening_balance: Money
|
||||
closing_balance: Money
|
||||
lowest_balance: Money
|
||||
lowest_balance_on: date | None
|
||||
|
||||
|
||||
class BudgetStatusOut(ApiModel):
|
||||
"""Stand eines Budgets samt Ampel."""
|
||||
|
||||
category_id: int
|
||||
category_name: str
|
||||
color: str
|
||||
period_month: date
|
||||
limit_amount: Money
|
||||
carried_over: Money = Field(description="Übertrag aus Vormonaten bei aktivem Rollover.")
|
||||
available: Money = Field(description="Limit zuzüglich Übertrag.")
|
||||
spent: Money
|
||||
remaining: Money
|
||||
ratio: float = Field(description="Verbrauchsanteil; 1.0 entspricht 100 %.")
|
||||
state: str = Field(description="'ok' unter 80 %, 'warning' unter 100 %, sonst 'exceeded'.")
|
||||
rollover: bool
|
||||
is_template: bool
|
||||
|
||||
|
||||
class SavingsGoalProgressOut(ApiModel):
|
||||
"""Fortschritt eines Sparziels."""
|
||||
|
||||
goal_id: int
|
||||
name: str
|
||||
color: str
|
||||
icon: str
|
||||
target_amount: Money
|
||||
current_amount: Money
|
||||
remaining_amount: Money
|
||||
ratio: float
|
||||
target_date: date | None
|
||||
months_left: int | None
|
||||
required_monthly: Money | None = Field(
|
||||
default=None, description="Nötige Rate bis zum Zieldatum."
|
||||
)
|
||||
monthly_contribution: Money | None
|
||||
is_on_track: bool | None = Field(
|
||||
default=None, description="Reicht die geplante Rate bis zum Zieldatum?"
|
||||
)
|
||||
|
||||
|
||||
class DashboardOut(ApiModel):
|
||||
"""Alles, was das Dashboard in einem Aufruf braucht."""
|
||||
|
||||
month: MonthReportOut
|
||||
total_balance: Money
|
||||
forecast: list[ForecastMonthOut]
|
||||
categories: list[CategorySliceOut]
|
||||
budgets: list[BudgetStatusOut]
|
||||
goals: list[SavingsGoalProgressOut]
|
||||
upcoming: list[CalendarEntryOut] = Field(
|
||||
description="Die nächsten Fälligkeiten der kommenden 14 Tage."
|
||||
)
|
||||
upcoming_deadlines: list[SubscriptionOut]
|
||||
|
||||
Reference in New Issue
Block a user