Files
certain/backend/app/schemas.py
T
GuidoandClaude Opus 4.8 1b7107b9bd feat: Phase 1 – Konten, Kategorien, Transaktionen, EZB-Währungsumrechnung
Backend:
- Modelle + Migration 0002: accounts, account_identifiers, category_groups,
  categories, transactions, exchange_rates (alle owner-gescoped).
- EZB-Kursdienst (90-Tage/Historie) + Backfill-Skript; Umrechnung EUR<->CHF zum
  Buchungsdatum mit lokalem Cache und manueller Übersteuerung; EUR-Pivot
  (amount_eur_cents) eingefroren.
- Kontostände + Netto-Vermögen (Umrechnung in Hauptwährung).
- Router: accounts (inkl. IBAN-Identifikatoren), categories (Gruppen+Kategorien),
  transactions (Ausgabe/Einnahme/Transfer, cross-currency), rates.
- Gruppierte Standard-Kategorien je Nutzer beim Anlegen.
- pytest-Suite: Währung, Salden, Cross-Currency-Transfer, Nutzer-Isolation.

Frontend:
- Seiten Konten, Kategorien, Buchungen; Icon-Chip, Geldbetrag mit Umrechnung,
  Icon-/Farbwähler, Modal; erweitertes SVG-Icon-Set (Konto-/Kategorie-Icons).
- Währungs-/Datumsformatierung (EUR de-DE, CHF de-CH), i18n DE/EN erweitert.
- Live-Umrechnungs-Vorschau im Transfer-Formular.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 18:59:24 +02:00

258 lines
6.6 KiB
Python

"""Pydantic-Schemas für Anfragen/Antworten (Phase 0: Auth, Benutzer, Einstellungen)."""
from datetime import date as date_t, datetime, time as time_t
from pydantic import BaseModel, ConfigDict, Field, field_validator
from .models import (
AccountType,
BudgetTreatment,
CategoryKind,
IdentifierKind,
Role,
TxnType,
)
ALLOWED_CURRENCIES = {"EUR", "CHF"}
def _check_currency(v: str) -> str:
v = v.upper()
if v not in ALLOWED_CURRENCIES:
raise ValueError("Währung muss EUR oder CHF sein")
return v
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
role: Role
display_name: str | None = None
is_active: bool
must_change_password: bool
created_at: datetime
class UserCreate(BaseModel):
username: str = Field(min_length=1, max_length=64)
password: str = Field(min_length=1, max_length=1024)
role: Role = Role.user
display_name: str | None = Field(default=None, max_length=120)
class UserUpdate(BaseModel):
password: str | None = Field(default=None, min_length=1, max_length=1024)
role: Role | None = None
display_name: str | None = Field(default=None, max_length=120)
is_active: bool | None = None
class PasswordChange(BaseModel):
current_password: str = Field(min_length=1, max_length=1024)
new_password: str = Field(min_length=1, max_length=1024)
class SettingOut(BaseModel):
key: str
value: str | None = None
# --- Konten ----------------------------------------------------------------
class AccountIdentifierIn(BaseModel):
kind: IdentifierKind = IdentifierKind.iban
value: str = Field(min_length=1, max_length=64)
class AccountIdentifierOut(BaseModel):
id: int
kind: IdentifierKind
value: str
class AccountCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
type: AccountType = AccountType.girokonto
currency: str = "EUR"
icon: str = "wallet"
color: str = "#3f51b5"
opening_balance_cents: int = 0
opening_date: date_t | None = None
identifiers: list[AccountIdentifierIn] = Field(default_factory=list)
@field_validator("currency")
@classmethod
def _cur(cls, v: str) -> str:
return _check_currency(v)
class AccountUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=120)
type: AccountType | None = None
currency: str | None = None
icon: str | None = None
color: str | None = None
opening_balance_cents: int | None = None
opening_date: date_t | None = None
is_archived: bool | None = None
position: int | None = None
# Wenn gesetzt, ersetzt die komplette Identifikator-Liste.
identifiers: list[AccountIdentifierIn] | None = None
@field_validator("currency")
@classmethod
def _cur(cls, v: str | None) -> str | None:
return _check_currency(v) if v is not None else None
class AccountOut(BaseModel):
id: int
name: str
type: AccountType
currency: str
icon: str
color: str
opening_balance_cents: int
opening_date: date_t | None
is_archived: bool
position: int
identifiers: list[AccountIdentifierOut]
balance_cents: int
balance_base_cents: int
# --- Kategorien ------------------------------------------------------------
class CategoryOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
group_id: int
name: str
type: CategoryKind
icon: str
color: str
position: int
is_builtin: bool
is_archived: bool
class CategoryGroupOut(BaseModel):
id: int
name: str
kind: CategoryKind
icon: str
color: str
position: int
is_builtin: bool
categories: list[CategoryOut]
class CategoryGroupCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
kind: CategoryKind = CategoryKind.expense
icon: str = "list"
color: str = "#6b7480"
class CategoryGroupUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=120)
icon: str | None = None
color: str | None = None
position: int | None = None
class CategoryCreate(BaseModel):
group_id: int
name: str = Field(min_length=1, max_length=120)
icon: str = "more-horizontal"
color: str = "#6b7480"
class CategoryUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=120)
group_id: int | None = None
icon: str | None = None
color: str | None = None
position: int | None = None
is_archived: bool | None = None
# --- Transaktionen ---------------------------------------------------------
class TransactionCreate(BaseModel):
type: TxnType
date: date_t
time: time_t | None = None
account_id: int
to_account_id: int | None = None
amount_cents: int = Field(gt=0)
to_amount_cents: int | None = Field(default=None, gt=0)
fx_rate: float | None = Field(default=None, gt=0)
category_id: int | None = None
budget_treatment: BudgetTreatment | None = None
budget_category_id: int | None = None
payee: str | None = Field(default=None, max_length=255)
description: str | None = Field(default=None, max_length=255)
notes: str | None = None
class TransactionUpdate(BaseModel):
type: TxnType | None = None
date: date_t | None = None
time: time_t | None = None
account_id: int | None = None
to_account_id: int | None = None
amount_cents: int | None = Field(default=None, gt=0)
to_amount_cents: int | None = Field(default=None, gt=0)
fx_rate: float | None = Field(default=None, gt=0)
category_id: int | None = None
budget_treatment: BudgetTreatment | None = None
budget_category_id: int | None = None
payee: str | None = Field(default=None, max_length=255)
description: str | None = Field(default=None, max_length=255)
notes: str | None = None
class TransactionOut(BaseModel):
id: int
type: TxnType
date: date_t
time: time_t | None
account_id: int
to_account_id: int | None
account_currency: str
amount_cents: int
to_amount_cents: int | None
fx_rate: float | None
amount_eur_cents: int
amount_base_cents: int
category_id: int | None
budget_treatment: BudgetTreatment | None
budget_category_id: int | None
payee: str | None
description: str | None
notes: str | None
class RateStatus(BaseModel):
currency: str
count: int
min_date: date_t | None
max_date: date_t | None
latest_rate: float | None
class ConversionOut(BaseModel):
amount_cents: int
from_currency: str
to_currency: str
date: date_t
converted_cents: int | None
rate: float | None