- Demo-Seed mit realistischem deutschem Haushalt (make seed-demo): drei Konten, zehn Firmen samt Logos, zwölf Posten, bestätigte Historie, Buchungen, Budgets und Sparziele; alle Termine relativ zum heutigen Monat - Tastaturkürzel n / r / / / ? samt Übersicht; greifen nur außerhalb von Eingabefeldern - Fehlergrenze für Renderfehler und eigene 404-Seite - Farbtoken beider Modi gegen WCAG geprüft und nachgezogen: Hilfstext lag hell bei 2,56:1, Weiß auf dem Primärknopf bei 3,30:1 – jetzt überall >= 4,5:1 - Abo-Übersicht heißt „Laufende Kosten“, weil sie auch Miete und Sparplan enthält - build.yml fällt auf GITEA_TOKEN zurück, wenn REGISTRY_TOKEN fehlt, und erklärt im Fehlerfall, wie das Secret anzulegen ist - README mit Screenshot-Abschnitt, Kürzeln und Barrierefreiheit Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
223 lines
5.9 KiB
Python
223 lines
5.9 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="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 `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
|
||
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]
|