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>
70 lines
3.4 KiB
Python
Executable File
70 lines
3.4 KiB
Python
Executable File
#!/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()
|