Files
GuidoandClaude Opus 5 d88efebfeb feat: Diagnose zeigt Stichtag und Beispieltexte je Übertrag
Der Stichtag eines Kontos wird von der Saldo-Berechnung ignoriert – das
Skript weist jetzt aus, wie viel aus Buchungen bis zum Stichtag stammt
und trotzdem mitzählt. Die Übertrags-Matrix nennt dazu Beispieltexte,
Erkennungstexte der Konten und Transfer-Schlagwörter, damit sich ein
falscher Übertrag seinem Auslöser zuordnen lässt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:22:59 +02:00

88 lines
4.7 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, opening_date 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']}) ===")
stichtag = a["opening_date"]
print(f" Anfangssaldo {geld(a['opening_balance_cents']):>14}"
f" Stichtag: {stichtag or '– keiner –'}")
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 '–'}")
# Der Stichtag wird von der Saldo-Berechnung derzeit ignoriert: sie addiert
# ALLE Buchungen. Gemeint ist aber "an diesem Tag stand so viel drauf", also
# dürften nur spätere Buchungen zählen. Hier steht, was das ausmacht.
if stichtag:
vor = (eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? "
"AND type='income' AND date <= ?", a["id"], stichtag)
- eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? "
"AND type='expense' AND date <= ?", a["id"], stichtag)
- eins("SELECT SUM(amount_cents) FROM transactions WHERE account_id=? "
"AND type='transfer' AND date <= ?", a["id"], stichtag)
+ eins("SELECT SUM(COALESCE(to_amount_cents, amount_cents)) FROM transactions "
"WHERE to_account_id=? AND type='transfer' AND date <= ?", a["id"], stichtag))
if vor:
print(f" ACHTUNG: {geld(vor)} stammt aus Buchungen BIS zum Stichtag und wird")
print(f" trotzdem mitgezählt. Ohne sie: {geld(saldo - vor, a['currency'])}")
# 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()