"""Kategorien, die nicht in Auswertungen zählen (z. B. „Kontoübertrag"). Eine Umbuchung zwischen eigenen Konten ist keine Ausgabe – das Geld liegt nur woanders. Der Kontostand muss sie trotzdem abbilden. Gemessen wird als Differenz: Die Testdatenbank lebt über den ganzen Lauf, es liegen also schon Buchungen anderer Tests im laufenden Monat.""" import datetime def _expense_cats(admin, n=2): out = [] for g in admin.get("/api/categories").json(): if g["kind"] == "expense": out += g["categories"] assert len(out) >= n return out[:n] def _ausgaben(admin): return admin.get("/api/dashboard/income-expense").json()["expense"] def _anteile(admin): d = admin.get("/api/dashboard/expense-share?group_by=category").json() return {r["name"]: r["amount"] for r in d["rows"]} def test_neutral_category_out_of_stats_but_in_balance(admin): acc = admin.post("/api/accounts", json={ "name": "Giro Neutral", "type": "girokonto", "currency": "EUR", "opening_balance_cents": 100000, }).json() normal, uebertrag = _expense_cats(admin, 2) for c in (normal, uebertrag): # definierter Ausgangszustand admin.patch(f"/api/categories/{c['id']}", json={"excluded_from_stats": False}) vorher_ausgaben = _ausgaben(admin) vorher_anteile = _anteile(admin) heute = datetime.date.today().isoformat() for cat, cents in ((normal, 5000), (uebertrag, 30000)): r = admin.post("/api/transactions", json={ "type": "expense", "date": heute, "account_id": acc["id"], "amount_cents": cents, "category_id": cat["id"], }) assert r.status_code == 201, r.text # Beide Buchungen wirken auf den Saldo: 1000.00 - 50.00 - 300.00 = 650.00 assert admin.get(f"/api/accounts/{acc['id']}").json()["balance_cents"] == 65000 assert _ausgaben(admin) == vorher_ausgaben + 35000 # Jetzt „Kontoübertrag" aus den Auswertungen nehmen. r = admin.patch(f"/api/categories/{uebertrag['id']}", json={"excluded_from_stats": True}) assert r.status_code == 200, r.text assert r.json()["excluded_from_stats"] is True # Nur noch die echte Ausgabe zählt – der Saldo bleibt unverändert. assert _ausgaben(admin) == vorher_ausgaben + 5000 assert admin.get(f"/api/accounts/{acc['id']}").json()["balance_cents"] == 65000 jetzt = _anteile(admin) assert jetzt.get(normal["name"], 0) == vorher_anteile.get(normal["name"], 0) + 5000 assert jetzt.get(uebertrag["name"], 0) == 0, "neutrale Kategorie darf nicht auftauchen" # Häkchen entfernen -> zählt sofort wieder mit. admin.patch(f"/api/categories/{uebertrag['id']}", json={"excluded_from_stats": False}) assert _ausgaben(admin) == vorher_ausgaben + 35000 def test_neutral_keyword_matches_both_directions(admin): """Ein Kontoübertrag geht auf dem einen Konto raus und kommt auf dem anderen rein. Die neutrale Kategorie muss deshalb für BEIDE Richtungen greifen – obwohl sie in einer Ausgabe-Gruppe liegt.""" import io, json acc = admin.post("/api/accounts", json={ "name": "Giro Beide", "type": "girokonto", "currency": "EUR", }).json() uebertrag = _expense_cats(admin, 2)[1] admin.patch(f"/api/categories/{uebertrag['id']}", json={"excluded_from_stats": True}) admin.post(f"/api/categories/{uebertrag['id']}/keywords", json={"word": "kontouebertrag"}) csv_text = ( "Datum;Betrag;Empfaenger;Zweck\n" "05.03.2026;-250,00;Kontouebertrag raus;;\n" "06.03.2026;250,00;Kontouebertrag rein;;\n" ) rows = admin.post( "/api/imports/preview", files={"file": ("k.csv", io.BytesIO(csv_text.encode("utf-8")), "text/csv")}, data={ "account_id": str(acc["id"]), "delimiter": ";", "decimal_sep": ",", "date_format": "%d.%m.%Y", "encoding": "utf-8", "has_header": "true", "skip_rows": "0", "amount_mode": "signed", "column_map": json.dumps({"date": 0, "amount": 1, "payee": 2, "purpose": 3}), }, ).json()["rows"] assert rows[0]["category_id"] == uebertrag["id"], "Ausgabe-Richtung" assert rows[1]["category_id"] == uebertrag["id"], "Einnahme-Richtung ebenso"