fix: Konto loeschen veraendert keine fremden Salden mehr

Beide Konto-Spalten einer Buchung hingen an ON DELETE CASCADE. Wurde ein
Zielkonto geloescht, verschwand die ganze Buchung – auch die Abgangsseite,
die einem anderen Konto gehoert. Dessen Saldo stieg dadurch stillschweigend
um den Uebertrag; beim Loeschen von 'Revolut investments' waren es 1'020.99.

Vor dem Loeschen werden solche Uebertraege jetzt zu gewoehnlicher Ausgabe
bzw. Einnahme auf dem verbleibenden Konto. Nur was allein am geloeschten
Konto haengt, geht mit ihm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 17:01:40 +02:00
co-authored by Claude Opus 5
parent e0955b79ee
commit 51af9eacff
2 changed files with 85 additions and 2 deletions
+47 -2
View File
@@ -7,9 +7,9 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, get_owned
from ..models import Account, AccountIdentifier, IdentifierKind, User
from ..models import Account, AccountIdentifier, IdentifierKind, Transaction, TxnType, User
from ..schemas import AccountCreate, AccountIdentifierOut, AccountOut, AccountUpdate
from ..services import balances
from ..services import balances, currency
from ..services.csv_import import normalize_text
from .settings import BASE_CURRENCY_KEY, get_user_setting
@@ -146,10 +146,55 @@ def update_account(
return to_out(db, acc, base_currency(db, user), date.today())
def _entkoppele_uebertraege(db: Session, acc: Account) -> None:
"""Überträge lösen, bevor das Konto verschwindet.
Beide Konto-Spalten einer Buchung hängen an ON DELETE CASCADE. Beim Löschen
eines Kontos verschwand deshalb auch die Gegenseite eines Übertrags – und
damit Geld, das ein ANDERES Konto verlassen hat. Dessen Saldo stieg
stillschweigend. Solche Überträge werden hier zu gewöhnlichen Ausgaben bzw.
Einnahmen auf dem verbleibenden Konto; nur was allein am gelöschten Konto
hängt, geht mit ihm.
"""
# Gelöschtes Konto ist das Ziel: Der Abgang bleibt Ausgabe beim Quellkonto.
for t in (
db.query(Transaction)
.filter(Transaction.to_account_id == acc.id, Transaction.account_id != acc.id)
.all()
):
t.type = TxnType.expense
t.to_account_id = None
t.to_amount_cents = None
# Gelöschtes Konto ist die Quelle: Der Zugang bleibt Einnahme beim Zielkonto.
for t in (
db.query(Transaction)
.filter(
Transaction.account_id == acc.id,
Transaction.to_account_id.isnot(None),
Transaction.to_account_id != acc.id,
)
.all()
):
ziel = db.get(Account, t.to_account_id)
betrag = t.to_amount_cents if t.to_amount_cents is not None else t.amount_cents
t.type = TxnType.income
t.account_id = t.to_account_id
t.amount_cents = betrag
t.to_account_id = None
t.to_amount_cents = None
if ziel is not None:
t.amount_eur_cents = currency.compute_amount_eur_cents(
db, betrag, ziel.currency, t.date
)
db.flush()
@router.delete("/{account_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_account(
account_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)
):
acc = get_owned(db, Account, account_id, user)
_entkoppele_uebertraege(db, acc)
db.delete(acc)
db.commit()