feat: Einer Buchung sieht man an, ob sie ein Uebertrag ist – und was sie war
Drei Dinge, die zusammengehoeren. 1. Uebertraege sind als solche lesbar. Stand nur eine Seite in der Liste – weil nach einem Konto gefiltert wird oder die andere nicht mitgeladen war – trug die Zeile den Rohtext der Bank als Ueberschrift. "To CHF MB:8f33c0aa-9c92-456b-a6fe-b64a858a019c" sah aus wie eine Zahlung an jemanden; die Verknuepfung stand klein darunter. Jetzt steht oben, zwischen WELCHEN Konten das Geld lief, in beiden Faellen gleich, und daneben eine Marke "Transfer". Ein Uebertrag ist weder Ausgabe noch Einnahme – das gehoert in die erste Zeile, nicht in eine Fussnote. 2. Notiz je Buchung. Das Feld gab es im Modell seit jeher, die Oberflaeche bot es nie an. Die Kategorie sagt, in welche Schublade etwas gehoert – nicht, WAS es war. "Amazon" kann Einrichtung sein, Werkzeug oder ein Geschenk, und in drei Monaten weiss das niemand mehr. Die Notiz steht in der Liste und ist mitdurchsuchbar: Wer sie schreibt, will spaeter danach suchen. 3. "Ignorieren" ist keine Einbahnstrasse mehr. Ein abgelehntes Paar blieb bei jedem weiteren Suchlauf stumm, und nichts verriet, dass es je einen Vorschlag gab. Wer einmal falsch klickte, suchte danach einen Fehler in der Erkennung, den es nicht gab. Die abgelehnten Paare lassen sich jetzt ansehen und zurueckholen. Und wenn die Vorschlaege nicht geladen werden koennen, wird das gesagt statt verschluckt – sonst sieht die Liste aus, als gaebe es nichts zu verbinden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
76dbd3b481
commit
eb72f0cb27
@@ -0,0 +1,137 @@
|
||||
"""Zwei Dinge, die es brauchte, damit man einer Buchung ansieht, was sie ist.
|
||||
|
||||
Die Notiz: Die Kategorie sagt, in welche Schublade es gehoert – nicht, WAS es
|
||||
war. „Amazon" kann Einrichtung sein, Werkzeug oder ein Geschenk.
|
||||
|
||||
Der Rueckweg aus „Ignorieren": Bisher war ein Klick endgueltig. Das Paar blieb
|
||||
bei jedem weiteren Suchlauf stumm, und nichts verriet, dass es je einen
|
||||
Vorschlag gab.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
|
||||
def _konto(admin, name, **kw):
|
||||
body = {"name": name, "type": "girokonto", "currency": "EUR"}
|
||||
body.update(kw)
|
||||
r = admin.post("/api/accounts", json=body)
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()
|
||||
|
||||
|
||||
def _buchung(admin, konto, art, betrag, wer, tag="2026-05-04", **kw):
|
||||
body = {"type": art, "date": tag, "account_id": konto["id"],
|
||||
"amount_cents": betrag, "payee": wer}
|
||||
body.update(kw)
|
||||
r = admin.post("/api/transactions", json=body)
|
||||
assert r.status_code in (200, 201), r.text
|
||||
return r.json()
|
||||
|
||||
|
||||
# --- Notiz ------------------------------------------------------------------
|
||||
|
||||
def test_notiz_laesst_sich_setzen_aendern_und_loeschen(admin):
|
||||
konto = _konto(admin, "Notiz-Konto")
|
||||
txn = _buchung(admin, konto, "expense", 4990, "Amazon",
|
||||
notes="Schraubenset, kein Moebelstueck")
|
||||
assert txn["notes"] == "Schraubenset, kein Moebelstueck"
|
||||
|
||||
r = admin.patch(f"/api/transactions/{txn['id']}", json={"notes": "Geschenk fuer Tim"})
|
||||
assert r.json()["notes"] == "Geschenk fuer Tim"
|
||||
|
||||
r = admin.patch(f"/api/transactions/{txn['id']}", json={"notes": None})
|
||||
assert r.json()["notes"] is None
|
||||
|
||||
|
||||
def test_notiz_ueberlebt_eine_aenderung_am_betrag(admin):
|
||||
"""Sie haengt an der Buchung, nicht an einer Nebensache."""
|
||||
konto = _konto(admin, "Notiz-Konto 2")
|
||||
txn = _buchung(admin, konto, "expense", 1000, "Amazon", notes="beruflich")
|
||||
r = admin.patch(f"/api/transactions/{txn['id']}", json={"amount_cents": 1200})
|
||||
assert r.json()["notes"] == "beruflich"
|
||||
|
||||
|
||||
def test_notiz_steht_in_der_liste(admin):
|
||||
"""Nach dem eigenen Konto gefragt – sonst haengt der Test an der Anzahl
|
||||
Buchungen, die andere Tests vorher angelegt haben."""
|
||||
konto = _konto(admin, "Notiz-Konto 3")
|
||||
_buchung(admin, konto, "expense", 2000, "Amazon", notes="Werkzeug")
|
||||
zeilen = admin.get(f"/api/transactions?account_id={konto['id']}").json()
|
||||
assert [t for t in zeilen if t.get("notes") == "Werkzeug"], "ohne das Feld in der Liste nuetzt die Notiz beim Suchen nichts"
|
||||
|
||||
|
||||
# --- Abgelehnte Vorschlaege -------------------------------------------------
|
||||
|
||||
def _paar(admin, name):
|
||||
a = _konto(admin, f"{name} A")
|
||||
b = _konto(admin, f"{name} B")
|
||||
# Mit Kategorie, damit nicht automatisch verknuepft wird: Dann bleibt es
|
||||
# ein Vorschlag, und genau den kann man ablehnen.
|
||||
kat = None
|
||||
for g in admin.get("/api/categories").json():
|
||||
for c in g["categories"]:
|
||||
if c["type"] == "expense":
|
||||
kat = c["id"]
|
||||
break
|
||||
if kat:
|
||||
break
|
||||
raus = _buchung(admin, a, "expense", 777, "Umbuchung X", category_id=kat)
|
||||
rein = _buchung(admin, b, "income", 777, "Umbuchung X")
|
||||
return raus, rein
|
||||
|
||||
|
||||
def test_abgelehntes_paar_laesst_sich_zurueckholen(admin):
|
||||
raus, rein = _paar(admin, "Ignoriert")
|
||||
|
||||
vor = admin.get("/api/uebertraege/vorschlaege").json()["vorschlaege"]
|
||||
assert any({p["a"]["id"], p["b"]["id"]} == {raus["id"], rein["id"]} for p in vor)
|
||||
|
||||
admin.post("/api/uebertraege/ignorieren",
|
||||
json={"a_id": raus["id"], "b_id": rein["id"]})
|
||||
|
||||
nach = admin.get("/api/uebertraege/vorschlaege").json()["vorschlaege"]
|
||||
assert not any({p["a"]["id"], p["b"]["id"]} == {raus["id"], rein["id"]} for p in nach)
|
||||
|
||||
# Und es ist auffindbar, statt spurlos zu verschwinden.
|
||||
liste = admin.get("/api/uebertraege/ignoriert").json()
|
||||
assert any({p["a"]["id"], p["b"]["id"]} == {raus["id"], rein["id"]} for p in liste)
|
||||
|
||||
r = admin.request("DELETE", "/api/uebertraege/ignoriert",
|
||||
json={"a_id": raus["id"], "b_id": rein["id"]})
|
||||
assert r.status_code == 204, r.text
|
||||
|
||||
wieder = admin.get("/api/uebertraege/vorschlaege").json()["vorschlaege"]
|
||||
assert any({p["a"]["id"], p["b"]["id"]} == {raus["id"], rein["id"]} for p in wieder)
|
||||
|
||||
|
||||
def test_ignoriert_zeigt_genug_zum_wiedererkennen(admin):
|
||||
raus, rein = _paar(admin, "Lesbar")
|
||||
admin.post("/api/uebertraege/ignorieren",
|
||||
json={"a_id": raus["id"], "b_id": rein["id"]})
|
||||
|
||||
eintrag = [p for p in admin.get("/api/uebertraege/ignoriert").json()
|
||||
if {p["a"]["id"], p["b"]["id"]} == {raus["id"], rein["id"]}][0]
|
||||
for seite in (eintrag["a"], eintrag["b"]):
|
||||
assert seite["date"] == "2026-05-04"
|
||||
assert seite["amount_cents"] == 777
|
||||
assert seite["payee"] == "Umbuchung X"
|
||||
assert seite["account_name"].startswith("Lesbar")
|
||||
|
||||
|
||||
def test_geloeschte_buchung_verschwindet_aus_der_liste(admin):
|
||||
"""Ein Eintrag, zu dem es nichts mehr zurueckzunehmen gibt, ist Rauschen."""
|
||||
raus, rein = _paar(admin, "Geloescht")
|
||||
admin.post("/api/uebertraege/ignorieren",
|
||||
json={"a_id": raus["id"], "b_id": rein["id"]})
|
||||
admin.delete(f"/api/transactions/{rein['id']}")
|
||||
|
||||
liste = admin.get("/api/uebertraege/ignoriert").json()
|
||||
assert not any(p["a"]["id"] == raus["id"] or p["b"]["id"] == raus["id"]
|
||||
for p in liste)
|
||||
|
||||
|
||||
def test_fremdes_paar_laesst_sich_nicht_zurueckholen(admin):
|
||||
assert date.today()
|
||||
r = admin.request("DELETE", "/api/uebertraege/ignoriert",
|
||||
json={"a_id": 999999, "b_id": 999998})
|
||||
assert r.status_code == 404, r.text
|
||||
Reference in New Issue
Block a user