Files
certain/backend/tests/test_neutral_category.py
T
GuidoandClaude Opus 4.8 af865a5c7b fix: neutrale Kategorien gelten für Ausgabe UND Einnahme
Kategorien erben ihre Art von der Gruppe, und die ist entweder Ausgabe oder
Einnahme. Für einen Kontoübertrag passt beides nicht: Auf dem einen Konto geht
Geld raus, auf dem anderen kommt es rein – man musste also zwei Kategorien in
zwei Gruppen anlegen.

Als „nicht in Auswertungen einrechnen" markierte Kategorien werden jetzt in
beiden Richtungen angeboten (Buchungsformular und Import-Prüfansicht), und ihre
Schlagwörter greifen unabhängig vom Vorzeichen. Eine Kategorie „Kontoübertrag"
in einer Ausgabe-Gruppe genügt damit für beide Seiten.

Test für beide Richtungen; 94 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 14:09:54 +02:00

102 lines
4.1 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.
"""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"