#!/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()