feat: Gegenseite eines Übertrags braucht keine eigene Transfer-Regel

Bisher musste die Regel auf BEIDEN Auszügen ein fremdes Gegenkonto liefern. Beim
Empfangs-Auszug zeigt sie aber auf das Konto, das gerade importiert wird – damit
blieb die Zeile ohne Gegenkonto und wurde still als Einnahme gebucht.

- Gegenbuchungen werden über die Konto-Seite bestehender Transfers gesucht, nicht
  über das Konto-Paar. Der Empfangs-Auszug braucht damit keine Regel mehr; eine
  Regel auf der abgehenden Seite genügt für beide Dateien.
- Trifft eine Transfer-Regel, ohne dass ein Gegenkonto feststeht, wird die Zeile
  nicht mehr als Einnahme/Ausgabe gebucht, sondern in der Prüfansicht mit
  „Gegenkonto wählen" markiert – sonst zählt die Bewegung je nach Import-
  Reihenfolge doppelt.
- Tests für beide Reihenfolgen; 86 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 08:20:13 +02:00
co-authored by Claude Opus 4.8
parent cb4f3a8909
commit 62eab06bdc
5 changed files with 69 additions and 33 deletions
+25 -5
View File
@@ -321,9 +321,9 @@ def test_currency_exchange_pairs_across_statements(admin):
chf = _account(admin, name="Rev CHF X", currency="CHF", opening_balance_cents=50000)
eur = _account(admin, name="Rev EUR X", currency="EUR", opening_balance_cents=0)
# Regeln: „umgetauscht" ist ein Transfer zum jeweils anderen Konto.
# EINE Regel genügt: „Umgetauscht in EUR" -> Gegenkonto ist das EUR-Konto.
admin.post("/api/categories/transfer-keywords",
json={"word": "umgetauscht", "target_account_id": eur["id"]})
json={"word": "umgetauscht in eur", "target_account_id": eur["id"]})
# 1) CHF-Auszug: Umtausch geht als Transfer CHF -> EUR raus.
rows = _rev_preview(admin, chf["id"], CHF_UMTAUSCH)
@@ -333,9 +333,8 @@ def test_currency_exchange_pairs_across_statements(admin):
admin.post("/api/imports/commit", json={"account_id": chf["id"], "filename": "chf.csv", "rows": rows})
assert admin.get(f"/api/accounts/{chf['id']}").json()["balance_cents"] == 40000
# 2) EUR-Auszug: dieselbe Bewegung – trotz anderem Betrag als Gegenbuchung erkannt.
admin.post("/api/categories/transfer-keywords",
json={"word": "umgetauscht", "target_account_id": chf["id"]})
# 2) EUR-Auszug: OHNE weitere Regel – die Gegenseite wird über den bereits
# gebuchten Transfer erkannt, obwohl der Betrag in EUR ein anderer ist.
rows2 = _rev_preview(admin, eur["id"], EUR_UMTAUSCH)
assert rows2[0]["type"] == "transfer"
assert rows2[0]["is_counterpart"] is True, "Umtausch-Gegenseite muss erkannt werden"
@@ -348,3 +347,24 @@ def test_currency_exchange_pairs_across_statements(admin):
assert admin.get(f"/api/accounts/{eur['id']}").json()["balance_cents"] == 10642
# CHF-Seite bleibt unverändert.
assert admin.get(f"/api/accounts/{chf['id']}").json()["balance_cents"] == 40000
def test_exchange_arrival_without_source_asks_instead_of_booking(admin):
"""Wird der Empfangs-Auszug zuerst eingelesen, ist die Quelle noch unbekannt.
Die Zeile darf dann NICHT still als Einnahme durchgehen, sondern fragt das
Konto ab – sonst zählt die Bewegung später doppelt."""
chf = _account(admin, name="Rev CHF Y", currency="CHF", opening_balance_cents=50000)
eur = _account(admin, name="Rev EUR Y", currency="EUR")
admin.post("/api/categories/transfer-keywords",
json={"word": "umgetauscht in eur", "target_account_id": eur["id"]})
rows = _rev_preview(admin, eur["id"], EUR_UMTAUSCH)
assert rows[0]["type"] == "transfer"
assert rows[0]["to_account_id"] is None
assert rows[0]["needs_target"] is True
assert rows[0]["include"] is False, "ohne Gegenkonto nicht automatisch buchen"
r = admin.post("/api/imports/commit", json={"account_id": eur["id"], "filename": "e.csv", "rows": rows})
assert r.json()["imported_count"] == 0
assert admin.get(f"/api/accounts/{eur['id']}").json()["balance_cents"] == 0
assert chf # Quellkonto bleibt unberührt