Files
certain/backend/scripts/pruefe_gegen_auszug.py
T
GuidoandClaude Opus 5 b4cd5b7ca3 feat: Auszugs-Vergleich nennt die fehlenden Buchungen einzeln
Die Tagessummen sagen nur, dass etwas fehlt. Jetzt werden die Betraege je
Tag als Menge verglichen: was im Auszug steht und nicht in Certain, und
was Certain zusaetzlich hat – Letzteres mit Art und Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:04:11 +02:00

151 lines
5.7 KiB
Python
Executable File
Raw 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
"""Vergleicht die gebuchten Daten Tag für Tag mit dem Original-Auszug.
Stimmt ein Kontostand nicht, sagt das nur DASS etwas fehlt. Dieses Skript sagt
WO: Es rechnet je Tag zusammen, was im Auszug steht und was in der Datenbank
gelandet ist, und listet die Tage auf, an denen beides auseinandergeht.
Aufruf (Spalten heissen wie in der Kopfzeile der Datei):
pruefe_gegen_auszug.py --konto 2 --datum "Datum des Abschlusses" --betrag Betrag \
--gebuehr "Gebühr" --status Status --filter "Produkt=Giro" auszug.csv [db]
"""
import argparse
import csv
import sqlite3
from collections import Counter, defaultdict
from datetime import date
NICHT_GEBUCHT = {"PENDING", "AUSSTEHEND", "REVERTED", "STORNIERT", "DECLINED",
"ABGELEHNT", "FAILED", "FEHLGESCHLAGEN", "CANCELLED"}
p = argparse.ArgumentParser()
p.add_argument("datei")
p.add_argument("db", nargs="?", default="/var/lib/certain-finance/app.db")
p.add_argument("--konto", type=int, required=True)
p.add_argument("--datum", required=True)
p.add_argument("--betrag", required=True)
p.add_argument("--gebuehr")
p.add_argument("--status")
p.add_argument("--filter", dest="filt")
p.add_argument("--trenner", default=",")
a = p.parse_args()
def zahl(s) -> int:
"""Betragstext -> Cent. Verträgt '1'234.56', '1.234,56' und leere Felder."""
s = str(s or "").strip().replace("'", "").replace(" ", "")
if not s:
return 0
if "," in s and "." in s:
s = s.replace(".", "").replace(",", ".") if s.rindex(",") > s.rindex(".") else s.replace(",", "")
elif "," in s:
s = s.replace(",", ".")
try:
return round(float(s) * 100)
except ValueError:
return 0
def tag(s) -> date | None:
s = str(s or "").strip()[:10]
for f in ("%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y", "%m/%d/%Y"):
try:
return date.strptime(s, f) if False else __import__("datetime").datetime.strptime(s, f).date()
except ValueError:
continue
return None
# --- Auszug einlesen ---------------------------------------------------------
fs, fv = (a.filt.split("=", 1) if a.filt else (None, None))
je_tag_datei: dict[date, int] = defaultdict(int)
zeilen_datei: dict[date, list] = defaultdict(list)
zeilen = 0
with open(a.datei, encoding="utf-8-sig", newline="") as f:
for r in csv.DictReader(f, delimiter=a.trenner):
if fs and (r.get(fs) or "").strip() != fv:
continue
if a.status and (r.get(a.status) or "").strip().upper() in NICHT_GEBUCHT:
continue
d = tag(r.get(a.datum))
if d is None:
continue
betrag = zahl(r.get(a.betrag)) - zahl(r.get(a.gebuehr)) if a.gebuehr else zahl(r.get(a.betrag))
je_tag_datei[d] += betrag
zeilen_datei[d].append(betrag)
zeilen += 1
# --- Datenbank ---------------------------------------------------------------
con = sqlite3.connect(f"file:{a.db}?mode=ro", uri=True)
je_tag_db: dict[date, int] = defaultdict(int)
zeilen_db: dict[date, list] = defaultdict(list)
for d, typ, betrag, ziel, zielbetrag, payee, besch in con.execute(
"SELECT date, type, amount_cents, to_account_id, to_amount_cents, "
"COALESCE(payee,''), COALESCE(description,'') FROM transactions "
"WHERE account_id = ? OR to_account_id = ?", (a.konto, a.konto)):
dd = tag(d)
if dd is None:
continue
if typ == "income":
wert = betrag
elif typ == "expense":
wert = -betrag
elif ziel == a.konto:
wert = zielbetrag if zielbetrag is not None else betrag
else:
wert = -betrag
je_tag_db[dd] += wert
zeilen_db[dd].append((wert, typ, (payee + " " + besch).strip()))
def geld(c):
return f"{c / 100:,.2f}".replace(",", "'")
print(f"Auszug: {a.datei} ({zeilen} gebuchte Zeilen)")
print(f"Konto {a.konto} in {a.db}\n")
print(f"Summe Auszug: {geld(sum(je_tag_datei.values())):>12}")
print(f"Summe Datenbank: {geld(sum(je_tag_db.values())):>12}")
print(f"Unterschied: {geld(sum(je_tag_db.values()) - sum(je_tag_datei.values())):>12}\n")
print("=== Monate mit Abweichung ===")
mon_d, mon_b = defaultdict(int), defaultdict(int)
for d, v in je_tag_datei.items():
mon_d[d.strftime("%Y-%m")] += v
for d, v in je_tag_db.items():
mon_b[d.strftime("%Y-%m")] += v
for m in sorted(set(mon_d) | set(mon_b)):
if mon_d[m] != mon_b[m]:
print(f" {m} Auszug {geld(mon_d[m]):>11} DB {geld(mon_b[m]):>11} "
f"Differenz {geld(mon_b[m] - mon_d[m]):>10}")
print("\n=== Tage mit Abweichung (die 25 grössten) ===")
tage = [(d, je_tag_db[d] - je_tag_datei[d]) for d in set(je_tag_datei) | set(je_tag_db)
if je_tag_db[d] != je_tag_datei[d]]
tage.sort(key=lambda t: -abs(t[1]))
for d, diff in tage[:25]:
print(f" {d} Auszug {geld(je_tag_datei[d]):>10} DB {geld(je_tag_db[d]):>10} "
f"Differenz {geld(diff):>10}")
if not tage:
print(" keine – der Auszug ist vollständig und korrekt gebucht.")
print("\n=== Welche Buchungen fehlen bzw. sind zu viel ===")
print("(Vergleich der einzelnen Betraege je Tag; der Auszug enthaelt keine Texte)")
for d, diff in tage[:10]:
aus, dbs = Counter(zeilen_datei[d]), Counter(w for w, _, _ in zeilen_db[d])
fehlt = sorted((aus - dbs).elements())
zuviel = sorted((dbs - aus).elements())
if not fehlt and not zuviel:
continue
print(f"\n {d} (Differenz {geld(diff)})")
if fehlt:
print(f" fehlt in Certain: {', '.join(geld(v) for v in fehlt)}")
if zuviel:
print(" zu viel in Certain:")
gezeigt = Counter()
for w, typ, text in sorted(zeilen_db[d], key=lambda x: x[0]):
if gezeigt[w] < Counter(zuviel)[w]:
gezeigt[w] += 1
print(f" {geld(w):>10} {typ:<9} {text[:52]}")