Wer nach dem Gehaltseingang plant, stellt unter Einstellungen den Tag ein, ab dem ein neuer Monat zählt. Der Zeitraum läuft dann vom Gehaltstag bis zum Vortag des nächsten und trägt den Namen des Monats, in dem er beginnt: Mit dem 25. umfasst „September 2026“ den 25.09. bis zum 24.10. Ein Starttag jenseits der Monatslänge rutscht auf den Monatsletzten, sodass 31 verlässlich den letzten Tag des Monats meint. Dashboard, Cashflow-Kalender, Budgets, Zwölf-Monats-Vorschau, die Kategorienauswertung, der Monatsexport und die Benachrichtigung über überschrittene Budgets rechnen mit diesem Zeitraum. Budgets bleiben je Monat gepflegt; der Bezeichner ist weiterhin der Monatserste, nur der Schnitt verschiebt sich. Bestandsinstallationen bleiben beim Ersten. Die Einstellung liegt in einer einzeiligen Tabelle hinter GET/PUT /api/settings; die Monatsauswertungen liefern zusätzlich period_start und period_end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fj7mB1PGA1aDHyfdSGHgzD
235 lines
6.5 KiB
Python
235 lines
6.5 KiB
Python
"""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="Bezeichner des Abrechnungsmonats – immer der Monatserste des Startmonats."
|
||
)
|
||
period_start: date = Field(description="Erster Tag des Zeitraums, also der Monatsbeginn.")
|
||
period_end: date = Field(description="Letzter Tag des Zeitraums.")
|
||
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 Abrechnungsmonat der Vorschau."""
|
||
|
||
month: date
|
||
period_start: date
|
||
period_end: date
|
||
income: Money
|
||
expenses: Money
|
||
balance: Money
|
||
cumulative_balance: Money = Field(
|
||
description="Prognostizierter Kontostand am Ende des Zeitraums ü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 `total_annual`, weil sie enden."
|
||
)
|
||
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 = Field(description="Bezeichner des Abrechnungsmonats – immer der Monatserste.")
|
||
period_start: date
|
||
period_end: date
|
||
days: list[CalendarDayOut] = Field(
|
||
description="Alle Tage des Zeitraums; bei abweichendem Monatsbeginn über zwei "
|
||
"Kalendermonate hinweg."
|
||
)
|
||
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_start_day: int = Field(description="Der eingestellte Monatsbeginn.")
|
||
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]
|