feat: Diagnose-Skript schlüsselt Kontostände auf

Zeigt je Konto Anfangssaldo, Einnahmen, Ausgaben und beide Transfer-
Richtungen einzeln, dazu mehrfach gebuchte Auszüge und doppelte
Buchungen. Damit lässt sich ein falscher Saldo auf seine Ursache
zurückführen, statt sie zu erraten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 14:58:38 +02:00
co-authored by Claude Opus 5
parent 3948d98273
commit 7fa15c2b86
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Schlüsselt auf, woraus sich jeder Kontostand zusammensetzt.
Saldo = Anfangssaldo + Einnahmen - Ausgaben - Transfers raus + Transfers rein.
Stimmt ein Kontostand nicht, liegt es fast immer an einer dieser fünf Zahlen –
oder an einem doppelt eingelesenen Auszug. Beides zeigt dieses Skript.
Aufruf: pruefe_salden.py [pfad-zur-db]
"""
import sqlite3
import sys
from collections import Counter
DB = sys.argv[1] if len(sys.argv) > 1 else "/var/lib/certain-finance/app.db"
con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
con.row_factory = sqlite3.Row
def eins(sql, *p):
return con.execute(sql, p).fetchone()[0] or 0
def geld(c, w=""):
return f"{c / 100:,.2f}".replace(",", "'") + (f" {w}" if w else "")
print(f"Datenbank: {DB}\n")
for a in con.execute("SELECT id, name, currency, opening_balance_cents FROM accounts "
"WHERE is_archived = 0 ORDER BY position, id"):
ein = eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? AND type='income'", a["id"])
aus = eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? AND type='expense'", a["id"])
raus = eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? AND type='transfer'", a["id"])
rein = eins("SELECT SUM(COALESCE(to_amount_cents, amount_cents)) FROM transactions "
"WHERE to_account_id=? AND type='transfer'", a["id"])
saldo = a["opening_balance_cents"] + ein - aus - raus + rein
anz = eins("SELECT COUNT(*) FROM transactions WHERE account_id=? OR to_account_id=?", a["id"], a["id"])
spanne = con.execute("SELECT MIN(date), MAX(date) FROM transactions WHERE account_id=?", (a["id"],)).fetchone()
print(f"=== {a['name']} ({a['currency']}, id {a['id']}) ===")
print(f" Anfangssaldo {geld(a['opening_balance_cents']):>14}")
print(f" + Einnahmen {geld(ein):>14}")
print(f" - Ausgaben {geld(aus):>14}")
print(f" - Transfers raus {geld(raus):>14}")
print(f" + Transfers rein {geld(rein):>14}")
print(f" = SALDO {geld(saldo, a['currency']):>14}")
print(f" {anz} Buchungen, {spanne[0] or '–'} bis {spanne[1] or '–'}")
# Doppelte Auszüge erkennen: gleicher Dateiname mehrfach gebucht.
batches = con.execute(
"SELECT filename, status, imported_count, created_at FROM import_batches "
"WHERE account_id=? ORDER BY created_at", (a["id"],)).fetchall()
namen = Counter(b["filename"] for b in batches if b["status"] == "committed")
for b in batches:
marke = " <-- Datei mehrfach gebucht!" if b["status"] == "committed" and namen[b["filename"]] > 1 else ""
print(f" [{b['status']:<9}] {b['imported_count']:>4} aus {b['filename'][:44]}{marke}")
# Buchungen, die sich in Datum+Betrag+Empfänger gleichen.
dop = con.execute(
"SELECT date, amount_cents, COALESCE(payee,''), COUNT(*) n FROM transactions "
"WHERE account_id=? GROUP BY 1,2,3 HAVING n > 1 ORDER BY n DESC LIMIT 5", (a["id"],)).fetchall()
if dop:
summe = eins("SELECT SUM(amount_cents * (n-1)) FROM (SELECT amount_cents, COUNT(*) n "
"FROM transactions WHERE account_id=? GROUP BY date, amount_cents, COALESCE(payee,'') "
"HAVING n > 1)", a["id"])
print(f" Mehrfach vorhanden: {len(dop)} Kombinationen, Summe der Überzähligen {geld(summe)}")
for d in dop:
print(f" {d[0]} {geld(d[1]):>10} {d[2][:34]:<34} x{d[3]}")
print()