diff --git a/backend/scripts/pruefe_salden.py b/backend/scripts/pruefe_salden.py new file mode 100755 index 0000000..da87059 --- /dev/null +++ b/backend/scripts/pruefe_salden.py @@ -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()