Files
certain/backend/app/services/balances.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

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
)