fix: Gegenbuchungs-Erkennung beachtet die Richtung
Die Suche prüfte nur Betrag und Zeitfenster. Ein erwarteter Transfer-EINGANG passte damit auch auf eine gleich hohe AUSGABE: Die echte Ausgabe galt als Gegenbuchung, wurde nicht gebucht und der Saldo lief um ihren Betrag zu hoch. Jetzt muss das Vorzeichen zur Seite des Transfers passen (wo Geld ankam, kann keine Ausgabe die Gegenseite sein). Test deckt genau diesen Fall ab; 87 grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -278,9 +278,14 @@ def _find_counterpart(
|
||||
absamt = abs(amount)
|
||||
unscharf = None
|
||||
for eintrag in side_index.get(account.id, []):
|
||||
txn_id, t_date, erwartet, other_id = eintrag
|
||||
txn_id, t_date, erwartet, other_id, richtung = eintrag
|
||||
if txn_id in verbraucht or abs((t_date - d).days) > COUNTERPART_DAYS:
|
||||
continue
|
||||
# Die Richtung muss stimmen: Wo Geld ankam, kann keine Ausgabe die
|
||||
# Gegenseite sein. Ohne diese Prüfung verschluckt eine Ausgabe über
|
||||
# denselben Betrag den erwarteten Eingang – der Saldo liefe zu hoch.
|
||||
if (amount < 0) != (richtung < 0):
|
||||
continue
|
||||
# Hat eine Regel ein Gegenkonto gesetzt, muss es dazu passen.
|
||||
if nur_konto is not None and other_id != nur_konto:
|
||||
continue
|
||||
@@ -380,8 +385,8 @@ def detect_rows(db: Session, user: User, account: Account, candidates: list[dict
|
||||
Transaction.to_account_id.isnot(None))
|
||||
):
|
||||
gutschrift = t_to_amt if t_to_amt is not None else t_amt
|
||||
side_index.setdefault(t_acc, []).append((t_id, t_date, t_amt, t_to))
|
||||
side_index.setdefault(t_to, []).append((t_id, t_date, gutschrift, t_acc))
|
||||
side_index.setdefault(t_acc, []).append((t_id, t_date, t_amt, t_to, -1))
|
||||
side_index.setdefault(t_to, []).append((t_id, t_date, gutschrift, t_acc, +1))
|
||||
verbraucht: set[int] = set()
|
||||
waehrung_je_konto: dict[int, str] = dict(
|
||||
db.query(Account.id, Account.currency).filter(Account.owner_id == user.id)
|
||||
|
||||
@@ -368,3 +368,41 @@ def test_exchange_arrival_without_source_asks_instead_of_booking(admin):
|
||||
assert r.json()["imported_count"] == 0
|
||||
assert admin.get(f"/api/accounts/{eur['id']}").json()["balance_cents"] == 0
|
||||
assert chf # Quellkonto bleibt unberührt
|
||||
|
||||
|
||||
def test_expense_is_not_swallowed_by_incoming_transfer(admin):
|
||||
"""Eine echte Ausgabe über denselben Betrag wie ein erwarteter Transfer-Eingang
|
||||
darf NICHT als Gegenbuchung gelten – sonst fehlt sie und der Saldo läuft zu hoch."""
|
||||
quelle = _account(admin, name="Quelle Z", currency="EUR", opening_balance_cents=100000)
|
||||
ziel = _account(admin, name="Ziel Z", currency="EUR", opening_balance_cents=10000)
|
||||
|
||||
# Transfer 50,00 von Quelle -> Ziel buchen.
|
||||
r = admin.post("/api/transactions", json={
|
||||
"type": "transfer", "date": "2026-03-10",
|
||||
"account_id": quelle["id"], "to_account_id": ziel["id"], "amount_cents": 5000,
|
||||
})
|
||||
assert r.status_code == 201, r.text
|
||||
|
||||
csv_text = (
|
||||
"Datum;Betrag;Empfaenger;Zweck;GegenIBAN\n"
|
||||
"11.03.2026;-50,00;Supermarkt;Einkauf;\n" # echte Ausgabe, gleicher Betrag
|
||||
"12.03.2026;50,00;Von Quelle;Uebertrag;\n" # das ist die echte Gegenbuchung
|
||||
)
|
||||
rows = admin.post(
|
||||
"/api/imports/preview",
|
||||
files={"file": ("z.csv", io.BytesIO(csv_text.encode("utf-8")), "text/csv")},
|
||||
data={
|
||||
"account_id": str(ziel["id"]), "delimiter": ";", "decimal_sep": ",",
|
||||
"date_format": "%d.%m.%Y", "encoding": "utf-8", "has_header": "true",
|
||||
"skip_rows": "0", "amount_mode": "signed", "column_map": COLMAP,
|
||||
},
|
||||
).json()["rows"]
|
||||
|
||||
assert rows[0]["is_counterpart"] is False, "Ausgabe ist keine Gegenbuchung"
|
||||
assert rows[0]["include"] is True
|
||||
assert rows[1]["is_counterpart"] is True, "der Eingang schon"
|
||||
assert rows[1]["include"] is False
|
||||
|
||||
admin.post("/api/imports/commit", json={"account_id": ziel["id"], "filename": "z.csv", "rows": rows})
|
||||
# 100.00 + 50.00 (Transfer) - 50.00 (Einkauf) = 100.00
|
||||
assert admin.get(f"/api/accounts/{ziel['id']}").json()["balance_cents"] == 10000
|
||||
|
||||
Reference in New Issue
Block a user