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:
@@ -0,0 +1,64 @@
|
||||
"""Kontostände (in Kontowährung) und Umrechnung in die Hauptwährung.
|
||||
|
||||
Saldo = Anfangssaldo + Einnahmen - Ausgaben - Transfers-raus + Transfers-rein.
|
||||
Transfers werden in der jeweiligen Kontowährung verrechnet (bei währungsüber-
|
||||
greifenden Transfers am Zielkonto der gutgeschriebene Betrag ``to_amount_cents``).
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import Account, Transaction, TxnType
|
||||
from . import currency
|
||||
|
||||
|
||||
def _sum(query) -> int:
|
||||
return int(query.scalar() or 0)
|
||||
|
||||
|
||||
def account_balance_cents(db: Session, account: Account) -> int:
|
||||
income = _sum(
|
||||
db.query(func.sum(Transaction.amount_cents)).filter(
|
||||
Transaction.account_id == account.id, Transaction.type == TxnType.income
|
||||
)
|
||||
)
|
||||
expense = _sum(
|
||||
db.query(func.sum(Transaction.amount_cents)).filter(
|
||||
Transaction.account_id == account.id, Transaction.type == TxnType.expense
|
||||
)
|
||||
)
|
||||
transfer_out = _sum(
|
||||
db.query(func.sum(Transaction.amount_cents)).filter(
|
||||
Transaction.account_id == account.id, Transaction.type == TxnType.transfer
|
||||
)
|
||||
)
|
||||
transfer_in = _sum(
|
||||
db.query(
|
||||
func.sum(func.coalesce(Transaction.to_amount_cents, Transaction.amount_cents))
|
||||
).filter(
|
||||
Transaction.to_account_id == account.id, Transaction.type == TxnType.transfer
|
||||
)
|
||||
)
|
||||
return account.opening_balance_cents + income - expense - transfer_out + transfer_in
|
||||
|
||||
|
||||
def account_balance_base_cents(
|
||||
db: Session, account: Account, base_currency: str, on: date
|
||||
) -> int:
|
||||
"""Saldo in die Hauptwährung umgerechnet (aktueller Kurs). Fällt auf den
|
||||
nativen Wert zurück, wenn kein Kurs verfügbar ist."""
|
||||
native = account_balance_cents(db, account)
|
||||
if account.currency == base_currency:
|
||||
return native
|
||||
conv = currency.convert(db, native, account.currency, base_currency, on)
|
||||
return conv if conv is not None else native
|
||||
|
||||
|
||||
def net_worth_cents(db: Session, accounts: list[Account], base_currency: str, on: date) -> int:
|
||||
return sum(
|
||||
account_balance_base_cents(db, a, base_currency, on)
|
||||
for a in accounts
|
||||
if not a.is_archived
|
||||
)
|
||||
Reference in New Issue
Block a user