Files
certain/backend/app/schemas.py
T
GuidoandClaude Opus 4.8 204a24d0df feat: Regeln (Betrag/Transfer/Konto-Texte) + Münz-Icon + pastellgrünes Theme
Regeln:
- Schlagwörter mit optionaler Betragsbedingung (z. B. „SV = 5.00 → Restaurant");
  Betrags-Regeln gewinnen vor betragslosen
- Transfer-Schlagwörter (Text → Transfer) auf der Konten-Seite
- Konto-Identifikator-Art „Text": erkennt das Zielkonto eines Transfers aus dem
  Buchungstext (längster passender Text gewinnt)
- Migration 0007, Endpunkte, Tests (Betrag/Transfer/Konflikt), gesamt 30 grün

Optik:
- App-Icon: Münzhaufen (aus Nutzer-SVG) weiß auf grünem Grund, klein gehalten
- Farbschema auf pastelliges Grün (Akzent hell/dunkel + Favicon)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 22:41:38 +02:00

388 lines
9.7 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,
AmountMode,
BudgetTreatment,
CategoryKind,
IdentifierKind,
ImportStatus,
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
# --- Budget: geplante Einnahmen --------------------------------------------
class PlannedIncomeCreate(BaseModel):
label: str = Field(min_length=1, max_length=120)
category_id: int | None = None
target_account_id: int | None = None
planned_cents: int = Field(ge=0)
class PlannedIncomeUpdate(BaseModel):
label: str | None = Field(default=None, min_length=1, max_length=120)
category_id: int | None = None
target_account_id: int | None = None
planned_cents: int | None = Field(default=None, ge=0)
class PlannedIncomeOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
label: str
category_id: int | None
target_account_id: int | None
planned_cents: int
position: int
# --- CSV-Import ------------------------------------------------------------
class ImportTemplateCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
delimiter: str = ";"
decimal_sep: str = ","
date_format: str = "%d.%m.%Y"
encoding: str = "utf-8"
has_header: bool = True
skip_rows: int = 0
amount_mode: AmountMode = AmountMode.signed
column_map: dict[str, int | None] = Field(default_factory=dict)
class ImportTemplateUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=120)
delimiter: str | None = None
decimal_sep: str | None = None
date_format: str | None = None
encoding: str | None = None
has_header: bool | None = None
skip_rows: int | None = None
amount_mode: AmountMode | None = None
column_map: dict[str, int | None] | None = None
class ImportTemplateOut(BaseModel):
id: int
name: str
delimiter: str
decimal_sep: str
date_format: str
encoding: str
has_header: bool
skip_rows: int
amount_mode: AmountMode
column_map: dict[str, int | None]
class ImportBatchOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
account_id: int
template_id: int | None
filename: str
row_count: int
imported_count: int
status: ImportStatus
created_at: datetime
class ImportCommitIn(BaseModel):
account_id: int
template_id: int | None = None
filename: str = ""
rows: list[dict] = Field(default_factory=list)
# --- Schlagwörter (manuelle Kategorisierungs-Regeln) -----------------------
class KeywordCreate(BaseModel):
word: str = Field(min_length=1, max_length=255)
amount_cents: int | None = Field(default=None, gt=0)
class KeywordUpdate(BaseModel):
word: str = Field(min_length=1, max_length=255)
amount_cents: int | None = Field(default=None, gt=0)
class KeywordOut(BaseModel):
id: int
category_id: int
word: str
amount_cents: int | None = None
class TransferKeywordCreate(BaseModel):
word: str = Field(min_length=1, max_length=255)
amount_cents: int | None = Field(default=None, gt=0)
class TransferKeywordOut(BaseModel):
id: int
word: str
amount_cents: int | None = None
class FlaggedKeywordOut(BaseModel):
id: int
category_id: int | None = None
category_name: str
word: str
applied_count: int
overridden_count: int