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>
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""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
|
|
)
|