feat: Pruefskript sagt, WELCHE der beiden Zeilen weg muss
Es liess die Entscheidung beim Nutzer, obwohl die Datenbank sie hergibt: In bank_account_links steht, welche Konten Certain selbst abholt. Sind beide angebunden, liefert die Bank beide Seiten und der Uebertrag ist falsch. Wird das Gegenkonto nicht abgerufen, ist der Uebertrag richtig und die zusaetzliche Buchung ueberzaehlig. Das steht jetzt je Fall dabei. Ausserdem war die Zusammenfassung irrefuehrend: Sie addierte Betraege in beide Richtungen. Eine zusaetzliche Gutschrift macht das Konto zu hoch, eine zusaetzliche Belastung zu niedrig – aus '2'026 zu viel' werden so '1'125 zu hoch'. Dazu zwei Tests zu der Frage, ob der naechste Abruf alles wieder anlegt: Nein. Der Umsatz wird an der Kennung der Bank wiedererkannt, unabhaengig davon, was aus der Buchung geworden ist, und ein spaeterer CSV-Auszug faellt ueber den Eingangs-Abgleich. Aufraeumen haelt also. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,15 @@ def geld(cents, waehrung=""):
|
|||||||
|
|
||||||
|
|
||||||
konten = {r["id"]: r for r in con.execute("SELECT id, name, currency FROM accounts")}
|
konten = {r["id"]: r for r in con.execute("SELECT id, name, currency FROM accounts")}
|
||||||
|
|
||||||
|
# Welche Konten holt Certain selbst von der Bank? Daran haengt, welche der
|
||||||
|
# beiden Zeilen die falsche ist – und das muss niemand raten.
|
||||||
|
try:
|
||||||
|
angebunden = {r[0] for r in con.execute(
|
||||||
|
"SELECT account_id FROM bank_account_links WHERE account_id IS NOT NULL")}
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
angebunden = set() # aeltere Datenbank ohne Bankanbindung
|
||||||
|
|
||||||
verdacht = []
|
verdacht = []
|
||||||
|
|
||||||
for u in con.execute("""
|
for u in con.execute("""
|
||||||
@@ -65,43 +74,53 @@ for u in con.execute("""
|
|||||||
WHERE account_id = ? AND type = ? AND date BETWEEN ? AND ? AND id != ?
|
WHERE account_id = ? AND type = ? AND date BETWEEN ? AND ? AND id != ?
|
||||||
""", (konto_id, richtung, von.isoformat(), bis.isoformat(), u["id"])):
|
""", (konto_id, richtung, von.isoformat(), bis.isoformat(), u["id"])):
|
||||||
if abs(k["amount_cents"] - betrag) <= spielraum:
|
if abs(k["amount_cents"] - betrag) <= spielraum:
|
||||||
verdacht.append((u, konto_id, betrag, k))
|
verdacht.append((u, konto_id, betrag, k, richtung))
|
||||||
|
|
||||||
if not verdacht:
|
if not verdacht:
|
||||||
print("Nichts gefunden – kein Übertrag hat eine zusätzlich gebuchte Gegenseite.")
|
print("Nichts gefunden – kein Übertrag hat eine zusätzlich gebuchte Gegenseite.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
print(f"{len(verdacht)} Stelle(n) zu prüfen.\n")
|
print(f"{len(verdacht)} Stelle(n) zu prüfen.\n")
|
||||||
summe = {}
|
abweichung = {}
|
||||||
for u, konto_id, betrag, k in verdacht:
|
for u, konto_id, betrag, k, richtung in verdacht:
|
||||||
konto = konten.get(konto_id, {"name": "?", "currency": ""})
|
konto = konten.get(konto_id, {"name": "?", "currency": ""})
|
||||||
a = konten.get(u["account_id"], {"name": "?"})
|
a = konten.get(u["account_id"], {"name": "?"})
|
||||||
b = konten.get(u["to_account_id"], {"name": "?"})
|
b = konten.get(u["to_account_id"], {"name": "?"})
|
||||||
|
gegen_id = u["to_account_id"] if konto_id == u["account_id"] else u["account_id"]
|
||||||
|
|
||||||
print(f"Übertrag #{u['id']} {u['date']} {a['name']} → {b['name']} "
|
print(f"Übertrag #{u['id']} {u['date']} {a['name']} → {b['name']} "
|
||||||
f"{geld(u['amount_cents'])}")
|
f"{geld(u['amount_cents'])}")
|
||||||
print(f" {u['payee'] or u['description'] or 'ohne Text'}")
|
print(f" {u['payee'] or u['description'] or 'ohne Text'}")
|
||||||
print(f" daneben auf '{konto['name']}': Buchung #{k['id']} {k['date']} "
|
print(f" daneben auf '{konto['name']}': Buchung #{k['id']} {k['date']} "
|
||||||
f"{geld(k['amount_cents'], konto['currency'])}")
|
f"{geld(k['amount_cents'], konto['currency'])}")
|
||||||
print(f" {k['payee'] or k['description'] or 'ohne Text'}")
|
print(f" {k['payee'] or k['description'] or 'ohne Text'}")
|
||||||
print(f" → dasselbe Geld zweimal auf '{konto['name']}'\n")
|
|
||||||
summe[konto_id] = summe.get(konto_id, 0) + k["amount_cents"]
|
|
||||||
|
|
||||||
print("Zu viel auf den Konten, wenn sich der Verdacht bestätigt:")
|
# Das Vorzeichen entscheidet, in welche Richtung das Konto danebenliegt.
|
||||||
for konto_id, cents in sorted(summe.items()):
|
vorzeichen = 1 if richtung == "income" else -1
|
||||||
|
zu = "zu hoch" if vorzeichen > 0 else "zu niedrig"
|
||||||
|
print(f" → '{konto['name']}' ist um {geld(k['amount_cents'])} {zu}")
|
||||||
|
|
||||||
|
if u["account_id"] in angebunden and u["to_account_id"] in angebunden:
|
||||||
|
print(f" → BEIDE Konten sind angebunden: Die Bank liefert beide Seiten "
|
||||||
|
f"selbst.\n Falsch ist der ÜBERTRAG #{u['id']} – löschen. Danach "
|
||||||
|
f"'{a['name']}' neu abrufen,\n dann verbindet Certain die beiden.")
|
||||||
|
elif gegen_id not in angebunden:
|
||||||
|
gegen = konten.get(gegen_id, {"name": "?"})
|
||||||
|
print(f" → '{gegen['name']}' wird NICHT abgerufen: Der Übertrag ist richtig.\n"
|
||||||
|
f" Überzählig ist Buchung #{k['id']} – die kann weg.")
|
||||||
|
else:
|
||||||
|
print(f" → Nur ein Konto ist angebunden. Sieh dir das Paar von Hand an.")
|
||||||
|
print()
|
||||||
|
abweichung[konto_id] = abweichung.get(konto_id, 0) + vorzeichen * k["amount_cents"]
|
||||||
|
|
||||||
|
print("Wenn sich der Verdacht bestätigt, liegen die Konten so daneben:")
|
||||||
|
for konto_id, cents in sorted(abweichung.items()):
|
||||||
konto = konten.get(konto_id, {"name": "?", "currency": ""})
|
konto = konten.get(konto_id, {"name": "?", "currency": ""})
|
||||||
print(f" {konto['name']:<28} {geld(cents, konto['currency'])}")
|
richtung = "zu hoch" if cents > 0 else "zu niedrig"
|
||||||
|
print(f" {konto['name']:<28} {geld(abs(cents), konto['currency'])} {richtung}")
|
||||||
|
|
||||||
print("""
|
print("""
|
||||||
So räumst du auf – je Paar EINE der beiden Zeilen, nicht beide:
|
Lösch je Paar nur EINE der beiden Zeilen – welche, steht oben.
|
||||||
|
|
||||||
Beide Seiten kommen von der Bank? Dann sind BEIDE Buchungen richtig, und
|
|
||||||
falsch ist nur der Übertrag. Lösch ihn und lass die zwei Buchungen von
|
|
||||||
Certain verbinden ("Zu erledigen" → Mögliche Überträge). Die Belastung, aus
|
|
||||||
der der Übertrag entstand, holst du mit einem erneuten Abruf des Kontos
|
|
||||||
zurück.
|
|
||||||
|
|
||||||
Das Gegenkonto holt Certain NICHT selbst ab? Dann ist der Übertrag richtig
|
|
||||||
und die zusätzliche Buchung überzählig – die kann weg.
|
|
||||||
|
|
||||||
Im Zweifel: Kontenseite ansehen. Steht dort "Saldo stimmt mit der Bank
|
Im Zweifel: Kontenseite ansehen. Steht dort "Saldo stimmt mit der Bank
|
||||||
überein", ist das Konto in Ordnung, egal was hier steht.""")
|
überein", ist das Konto in Ordnung, egal was hier steht.""")
|
||||||
|
|||||||
@@ -928,3 +928,61 @@ def test_ohne_angabe_wird_nichts_geraten(admin, fake_bank):
|
|||||||
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
|
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
|
||||||
assert eintrag["fx_rate"] is None
|
assert eintrag["fx_rate"] is None
|
||||||
assert eintrag["counter_amount_cents"] is None
|
assert eintrag["counter_amount_cents"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_geloeschte_buchung_kommt_beim_naechsten_abruf_nicht_zurueck(admin, fake_bank):
|
||||||
|
"""Die Frage beim Aufraeumen: Legt der naechste Abruf alles wieder an?
|
||||||
|
|
||||||
|
Nein – der Umsatz wird an der Kennung der Bank wiedererkannt, unabhaengig
|
||||||
|
davon, was aus der Buchung geworden ist. Das ist Absicht: Wer etwas
|
||||||
|
weggeworfen hat, will es nicht beim naechsten Abruf zurueck.
|
||||||
|
"""
|
||||||
|
heute = date.today().isoformat()
|
||||||
|
verbindung, link, _ = _verbunden(admin, fake_bank,
|
||||||
|
umsaetze=[dict(UMSATZ, booking_date=heute)])
|
||||||
|
konto = _konto(admin, name=f"Aufraeumen {secrets.token_hex(3)}", currency="EUR")
|
||||||
|
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
|
||||||
|
|
||||||
|
admin.post(f"/api/bank/connections/{verbindung}/sync")
|
||||||
|
eintrag = [e for e in admin.get("/api/inbox").json()
|
||||||
|
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
|
||||||
|
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -1490
|
||||||
|
|
||||||
|
# So raeumt man auf: die Buchung loeschen.
|
||||||
|
assert admin.delete(f"/api/transactions/{eintrag['transaction_id']}").status_code == 204
|
||||||
|
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 0
|
||||||
|
|
||||||
|
# Und jetzt derselbe Umsatz noch einmal von der Bank.
|
||||||
|
zweiter = admin.post(f"/api/bank/connections/{verbindung}/sync").json()
|
||||||
|
assert zweiter["geholt"] >= 1, "der Umsatz kam wieder an"
|
||||||
|
assert zweiter["neu"] == 0, "wurde aber nicht erneut angelegt"
|
||||||
|
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_geloeschte_buchung_blockiert_auch_den_csv_import(admin, fake_bank):
|
||||||
|
"""Der andere Weg herein: Ein spaeterer Auszug darf sie auch nicht bringen."""
|
||||||
|
heute = date.today()
|
||||||
|
verbindung, link, _ = _verbunden(
|
||||||
|
admin, fake_bank, umsaetze=[dict(UMSATZ, booking_date=heute.isoformat())])
|
||||||
|
konto = _konto(admin, name=f"CSV-Schutz {secrets.token_hex(3)}", currency="EUR")
|
||||||
|
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
|
||||||
|
admin.post(f"/api/bank/connections/{verbindung}/sync")
|
||||||
|
eintrag = [e for e in admin.get("/api/inbox").json()
|
||||||
|
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
|
||||||
|
admin.delete(f"/api/transactions/{eintrag['transaction_id']}")
|
||||||
|
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models import Account, User
|
||||||
|
from app.services import csv_import
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
nutzer = db.query(User).order_by(User.id).first()
|
||||||
|
acc = db.get(Account, konto["id"])
|
||||||
|
zeilen = csv_import.detect_rows(db, nutzer, acc, [{
|
||||||
|
"date": heute.isoformat(), "amount_cents": -1490,
|
||||||
|
"payee": "SPEEDEE RESTAURATIONS ZUERICH", "purpose": None}])
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
assert zeilen[0]["is_duplicate"] is True
|
||||||
|
assert zeilen[0]["duplicate_reason"] == "inbox"
|
||||||
|
|||||||
Reference in New Issue
Block a user