Files
certain/backend/tests/test_notiz_und_ignoriert.py
T
ScarriffleandClaude Opus 5 eb72f0cb27 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>
2026-10-01 20:49:01 +02:00

138 lines
5.5 KiB
Python
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.
"""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