Files
certain/backend/app/services/balances.py
T
GuidoandClaude Opus 5 5dbfd6abc4 fix: Anfangssaldo gilt ab dem Stichtag
Der Stichtag eines Kontos wurde gespeichert und angezeigt, von der
Saldo-Berechnung aber ignoriert: sie addierte jede Buchung des Kontos,
auch die aus der Zeit vor dem Stichtag. Damit lief jeder Saldo um die
gesamte Vorgeschichte daneben.

Der Anfangssaldo beschreibt jetzt den Stand am ENDE des Stichtags – so
steht er auf dem Auszug. Buchungen bis einschliesslich dieses Tages
zaehlen nicht mehr zum Kontostand, bleiben aber in Liste und
Auswertungen. Beim Uebertrag wird jede Seite an ihrem eigenen Stichtag
gemessen; der Vermoegensverlauf folgt derselben Regel. Ohne Stichtag
zaehlt weiterhin alles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:26:21 +02:00

82 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Kontostände (in Kontowährung) und Umrechnung in die Hauptwährung.
Saldo = Anfangssaldo + Einnahmen - Ausgaben - Transfers-raus + Transfers-rein,
gezählt ab dem Stichtag des Kontos (siehe ``counts_after_opening``).
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 counts_after_opening(account: Account):
"""Bedingung, die Buchungen bis zum Stichtag ausschliesst.
Der Anfangssaldo beschreibt den Stand *am Ende* des Stichtags – so steht er
auf jedem Auszug und im E-Banking. Buchungen bis einschliesslich dieses Tages
stecken also schon darin und dürfen nicht noch einmal zählen. Sie bleiben in
der Liste und in den Auswertungen, nur der Kontostand lässt sie aus.
Ohne Stichtag zählt alles – dann ist der Anfangssaldo der Stand von Beginn an.
"""
if account.opening_date is None:
return None
return Transaction.date > account.opening_date
def account_balance_cents(db: Session, account: Account) -> int:
ab = counts_after_opening(account)
def summe(spalte, *bedingungen) -> int:
query = db.query(func.sum(spalte)).filter(*bedingungen)
if ab is not None:
query = query.filter(ab)
return _sum(query)
income = summe(
Transaction.amount_cents,
Transaction.account_id == account.id, Transaction.type == TxnType.income,
)
expense = summe(
Transaction.amount_cents,
Transaction.account_id == account.id, Transaction.type == TxnType.expense,
)
transfer_out = summe(
Transaction.amount_cents,
Transaction.account_id == account.id, Transaction.type == TxnType.transfer,
)
transfer_in = summe(
func.coalesce(Transaction.to_amount_cents, Transaction.amount_cents),
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
)