Files
moneyfy/backend/app/schemas/report.py
T
moneyfyandClaude Opus 5 0adf154049 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
2026-09-09 16:50:22 +02:00

223 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Schemata der Auswertungen."""
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):
"""Einnahmen, Ausgaben und Saldo einer Sicht."""
income: Money
expenses: Money
balance: Money
class MonthComparisonOut(ApiModel):
"""Veränderung gegenüber dem Vormonat."""
income: Money
expenses: Money
balance: Money
class MonthReportOut(ApiModel):
"""Monatsübersicht mit Plan-Ist-Vergleich."""
month: date = Field(description="Immer der Monatserste.")
planned: TotalsOut = Field(description="Soll aus Fälligkeiten und Buchungen.")
actual: TotalsOut = Field(
description="Ist aus bestätigten Fälligkeiten und allen einmaligen Buchungen."
)
previous_planned: TotalsOut
previous_actual: TotalsOut
delta_to_previous: MonthComparisonOut
fixed_costs: Money = Field(description="Ausgaben in Kategorien mit `is_fixed_cost`.")
variable_costs: Money
reserves: Money = Field(description="Summe der monatlichen Rücklagen.")
available_after_fixed: Money = Field(description="Einkünfte abzüglich Fixkosten und Rücklagen.")
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]