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>
This commit is contained in:
+217
-3
@@ -1,10 +1,26 @@
|
||||
"""Pydantic-Schemas für Anfragen/Antworten (Phase 0: Auth, Benutzer, Einstellungen)."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import date as date_t, datetime, time as time_t
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from .models import Role
|
||||
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):
|
||||
@@ -41,3 +57,201 @@ class PasswordChange(BaseModel):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user