Files
moneyfy/backend/app/schemas/report.py
T
Jonas MenzelandClaude Opus 5 0a6261fc55
CI / backend (push) Successful in 2m33s
Images bauen / build (backend) (push) Successful in 3m26s
Images bauen / build (frontend) (push) Successful in 4m4s
CI / frontend (push) Successful in 6m32s
feat: einstellbarer Monatsbeginn zum Gehaltstag
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
2026-09-10 09:52:51 +02:00

235 lines
6.5 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="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]