Seit der Import keine Konten mehr verbindet, wird aus jedem Uebertrag ein Paar: Ausgabe beim einen Konto, Einnahme beim anderen. Zaehlte nur die Ausgabe nicht, stuende das Geld als Einnahme in der Auswertung und die Monatsbilanz waere um den vollen Betrag zu gut. Der Test belegt: eine neutrale Kategorie aus einer AUSGABEN-Gruppe laesst sich einer Einnahme zuweisen, der Kontostand steigt, die Einnahmen der Auswertung bleiben unveraendert – und ohne die Markierung zaehlt dieselbe Buchung wieder mit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
5.9 KiB
Python
143 lines
5.9 KiB
Python
"""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"
|
||
|
||
|
||
def _einnahmen(admin):
|
||
return admin.get("/api/dashboard/income-expense").json()["income"]
|
||
|
||
|
||
def test_neutrale_kategorie_gilt_auch_fuer_einnahmen(admin):
|
||
"""Die Gegenseite eines Kontoübertrags ist eine Einnahme – und darf ebenso
|
||
wenig in die Statistik einfliessen wie die Ausgabe.
|
||
|
||
Seit der Import keine Konten mehr verbindet, wird aus jedem Übertrag ein
|
||
Paar: Ausgabe beim einen Konto, Einnahme beim anderen. Zählte nur die
|
||
Ausgabe nicht, stünde das Geld als Einnahme in der Auswertung und die
|
||
Bilanz des Monats wäre um den vollen Betrag zu gut.
|
||
"""
|
||
acc = admin.post("/api/accounts", json={
|
||
"name": "Giro Neutral Einnahme", "type": "girokonto", "currency": "EUR",
|
||
"opening_balance_cents": 0,
|
||
}).json()
|
||
# Neutrale Kategorie liegt in einer AUSGABEN-Gruppe – sie muss trotzdem
|
||
# für eine Einnahme verwendbar sein.
|
||
uebertrag, = _expense_cats(admin, 1)
|
||
admin.patch(f"/api/categories/{uebertrag['id']}", json={"excluded_from_stats": True})
|
||
|
||
vorher = _einnahmen(admin)
|
||
heute = str(datetime.date.today())
|
||
r = admin.post("/api/transactions", json={
|
||
"type": "income", "date": heute, "account_id": acc["id"],
|
||
"amount_cents": 25000, "category_id": uebertrag["id"],
|
||
})
|
||
assert r.status_code == 201, r.text
|
||
|
||
# Der Kontostand steigt ...
|
||
assert admin.get(f"/api/accounts/{acc['id']}").json()["balance_cents"] == 25000
|
||
# ... die Einnahmen der Auswertung nicht.
|
||
assert _einnahmen(admin) == vorher
|
||
|
||
# Ohne die Markierung zählt dieselbe Buchung wieder mit.
|
||
admin.patch(f"/api/categories/{uebertrag['id']}", json={"excluded_from_stats": False})
|
||
assert _einnahmen(admin) == vorher + 25000
|
||
admin.patch(f"/api/categories/{uebertrag['id']}", json={"excluded_from_stats": True})
|