feat: Regeln nachtraeglich auf Buchungen ohne Kategorie anwenden
Regeln greifen beim Import. Alles, was vorher hereinkam – oder hereinkam, bevor es die Regel gab –, bleibt liegen. Bei einer Bankanbindung passiert genau das dauernd: Die Umsaetze kommen von selbst, die Regel entsteht spaeter. Die Revolut-Aufrundungen aufs Sparen-Pocket sind der Fall, an dem es aufgefallen ist. Der Knopf steht bei der Transfer-Erkennung und zeigt erst, was passieren wuerde. Uebertrags-Regeln laufen durch denselben Dienst wie jeder andere Uebertrag – er lehnt ab, wo die Gegenseite schon gebucht ist, und solche Buchungen bleiben unangetastet und werden mit Grund gemeldet. Ohne festes Zielkonto wird es aus dem Buchungstext erraten, aber nur bei GENAU einem Treffer und nur an Wortgrenzen: "Euro" darf nicht "PayPal Europe" treffen. Zwei Moeglichkeiten heisst raten, und dann passiert nichts. Angefasst werden nur Buchungen OHNE Kategorie – sonst ueberschriebe der Knopf stillschweigend, was von Hand zugeordnet wurde. Ein Uebertrag ist kein Kandidat mehr, ein zweiter Klick verschiebt das Geld also nicht erneut. Zwei Dinge fielen beim Testen auf und sind mit behoben: - Ein fehlgeschlagener Uebertrag verwarf per rollback() auch alles, was im selben Durchlauf schon gelungen war. Jetzt haelt ein Rettungspunkt je Uebertrag den Schaden bei dem einen. - test_neutral_category nahm VORHANDENE Kategorien und stellte eine davon auf "nicht einrechnen". Benutzten andere Buchungen sie, fielen die mit heraus und die Rechnung ging um deren Summe daneben – ohne Bezug zum Gepruefften. Der Test legt sich seine Kategorien jetzt selbst an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ längstes gewinnt, Kind-Filter, Korrektur-Erkennung/Flagging, keine Rückwirkung
|
||||
|
||||
import io
|
||||
import json
|
||||
import secrets
|
||||
|
||||
|
||||
def _account(admin, **kw):
|
||||
@@ -350,3 +351,71 @@ def test_same_word_multiple_amounts(admin):
|
||||
json={"word": "sv gruppe", "amount_cents": 500})
|
||||
assert doppelt.status_code == 201
|
||||
assert len(admin.get(f"/api/categories/{mensa['id']}/keywords").json()) == len(worte)
|
||||
|
||||
|
||||
def _kto(admin, **kw):
|
||||
body = {"name": f"K {secrets.token_hex(3)}", "type": "girokonto",
|
||||
"currency": "CHF", "opening_balance_cents": 0}
|
||||
body.update(kw)
|
||||
return admin.post("/api/accounts", json=body).json()
|
||||
|
||||
|
||||
def test_regeln_lassen_sich_nachtraeglich_anwenden(admin):
|
||||
"""Regeln greifen sonst nur beim Import. Bei einer Bankanbindung kommen die
|
||||
Umsaetze von selbst und die Regel entsteht spaeter – dann bleibt alles
|
||||
liegen, was vorher schon da war."""
|
||||
quelle = _kto(admin, name=f"Revolut {secrets.token_hex(3)}")
|
||||
pocket = _kto(admin, name=f"Sparen Pocket {secrets.token_hex(3)}")
|
||||
kennung = f"to chf mb:{secrets.token_hex(6)}"
|
||||
|
||||
# Drei Aufrundungen, wie Revolut sie liefert – ohne Kategorie.
|
||||
ids = []
|
||||
for cent in (100, 50, 34):
|
||||
ids.append(admin.post("/api/transactions", json={
|
||||
"type": "expense", "date": "2026-08-25", "account_id": quelle["id"],
|
||||
"amount_cents": cent, "payee": kennung.upper()}).json()["id"])
|
||||
|
||||
r = admin.post("/api/categories/transfer-keywords", json={
|
||||
"word": kennung, "target_account_id": pocket["id"]})
|
||||
assert r.status_code == 201, r.text
|
||||
|
||||
# Erst zeigen, nichts tun. Die Zahlen gelten fuer ALLE offenen Buchungen
|
||||
# des Nutzers, deshalb wird hier nur geprueft, dass nichts passiert ist.
|
||||
vor = admin.post("/api/categories/keywords/anwenden", params={"vorschau": True}).json()
|
||||
assert vor["uebertraege"] >= 3
|
||||
assert admin.get(f"/api/accounts/{pocket['id']}").json()["balance_cents"] == 0
|
||||
for txn_id in ids:
|
||||
assert admin.get(f"/api/transactions/{txn_id}").json()["type"] == "expense"
|
||||
|
||||
# Dann wirklich.
|
||||
admin.post("/api/categories/keywords/anwenden", params={"vorschau": False})
|
||||
assert admin.get(f"/api/accounts/{pocket['id']}").json()["balance_cents"] == 184
|
||||
assert admin.get(f"/api/accounts/{quelle['id']}").json()["balance_cents"] == -184
|
||||
for txn_id in ids:
|
||||
assert admin.get(f"/api/transactions/{txn_id}").json()["type"] == "transfer"
|
||||
|
||||
# Ein zweiter Lauf ruehrt sie nicht noch einmal an: Ein Uebertrag ist kein
|
||||
# Kandidat mehr, sonst verschoebe jeder Klick das Geld erneut.
|
||||
admin.post("/api/categories/keywords/anwenden", params={"vorschau": False})
|
||||
assert admin.get(f"/api/accounts/{pocket['id']}").json()["balance_cents"] == 184
|
||||
|
||||
|
||||
def test_nachtraegliches_anwenden_ruehrt_kategorisiertes_nicht_an(admin):
|
||||
"""Nur Buchungen OHNE Kategorie – sonst überschriebe der Knopf stillschweigend
|
||||
das, was man von Hand zugeordnet hat."""
|
||||
konto = _kto(admin)
|
||||
gruppen = admin.get("/api/categories").json()
|
||||
kat = next(c["id"] for g in gruppen if g["kind"] == "expense"
|
||||
for c in g["categories"])
|
||||
wort = f"marke{secrets.token_hex(4)}"
|
||||
txn = admin.post("/api/transactions", json={
|
||||
"type": "expense", "date": "2026-08-25", "account_id": konto["id"],
|
||||
"amount_cents": 999, "payee": wort, "category_id": kat}).json()
|
||||
|
||||
andere = next(c["id"] for g in gruppen if g["kind"] == "expense"
|
||||
for c in g["categories"] if c["id"] != kat)
|
||||
admin.post(f"/api/categories/{andere}/keywords",
|
||||
json={"pattern": wort, "match_field": "any"})
|
||||
|
||||
admin.post("/api/categories/keywords/anwenden", params={"vorschau": False})
|
||||
assert admin.get(f"/api/transactions/{txn['id']}").json()["category_id"] == kat
|
||||
|
||||
@@ -10,12 +10,24 @@ import datetime
|
||||
|
||||
|
||||
def _expense_cats(admin, n=2):
|
||||
"""EIGENE Kategorien anlegen, keine vorhandenen nehmen.
|
||||
|
||||
Der Test stellt eine davon spaeter auf "nicht einrechnen". Nimmt er eine
|
||||
Kategorie, die andere Buchungen schon benutzen, fallen die mit heraus – und
|
||||
die Rechnung geht um deren Summe daneben, ohne dass es mit dem zu tun hat,
|
||||
was hier geprueft wird.
|
||||
"""
|
||||
import secrets
|
||||
|
||||
gruppe = next(g for g in admin.get("/api/categories").json()
|
||||
if g["kind"] == "expense")
|
||||
out = []
|
||||
for g in admin.get("/api/categories").json():
|
||||
if g["kind"] == "expense":
|
||||
out += g["categories"]
|
||||
assert len(out) >= n
|
||||
return out[:n]
|
||||
for i in range(n):
|
||||
r = admin.post("/api/categories", json={
|
||||
"group_id": gruppe["id"], "name": f"Neutral-Test {i} {secrets.token_hex(3)}"})
|
||||
assert r.status_code == 201, r.text
|
||||
out.append(r.json())
|
||||
return out
|
||||
|
||||
|
||||
def _ausgaben(admin):
|
||||
|
||||
Reference in New Issue
Block a user