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>
88 lines
4.7 KiB
Python
Executable File
88 lines
4.7 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, 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()
|