feat: Kurs der Bank statt EZB-Naeherung, und Saldo uebernehmen

Bei einer Fremdwaehrungs-Zahlung nennt die Bank oft ihren echten Kurs und den
Betrag in der Ausgangswaehrung. Certain warf das weg und naeherte mit dem
EZB-Kurs. Das faellt genau dort auf, wo es teuer ist: Die spaeter gelieferte
Gegenseite eines Uebertrags laesst sich mit einem geschaetzten Betrag nicht als
dieselbe Bewegung wiedererkennen.

Der Uebertrag waehlt jetzt in drei Stufen: der von der Bank genannte
Gegenbetrag (exakt), sonst ihr Kurs, sonst die EZB. Nur die erste Stufe gilt
als bekannt – daran haengt, ob bei einem angebundenen Zielkonto nachgefragt
wird.

Der Parser klopft mehrere Formen ab, weil die Feldnamen je Anbindung
verschieden sind, und setzt still nichts, wenn keine passt. Keine dieser Formen
ist gegen echte Daten geprueft; ein falscher Kurs saehe aus wie ein richtiger,
deshalb wird lieber gar keiner gesetzt.

Ausserdem ein Knopf auf der Kontenseite: den Saldo der Bank als richtig
uebernehmen. Die Differenz wandert in den ANFANGSBESTAND – sie steht fuer
Bewegungen, die Certain nie gesehen hat, und genau dafuer ist er da. Eine
Korrekturbuchung stuende dagegen fuer immer in der Liste und liefe durch jede
Auswertung. Die Buchungen bleiben unangetastet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 10:18:33 +02:00
co-authored by Claude Opus 5
parent 8ceee5c5b1
commit 8309f58a67
12 changed files with 297 additions and 16 deletions
+62 -2
View File
@@ -309,6 +309,61 @@ def _betrag_cents(umsatz: dict) -> int | None:
return cents
def _betrag_mit_waehrung(wert) -> tuple[int, str] | None:
"""Aus ``{"amount": "12.00", "currency": "EUR"}`` Cent und Waehrung."""
if not isinstance(wert, dict):
return None
cents = csv_import.parse_amount(str(wert.get("amount")), ".")
waehrung = wert.get("currency")
if cents is None or not waehrung:
return None
return abs(cents), str(waehrung).upper()[:3]
def _umrechnung(umsatz: dict, waehrung: str | None) -> tuple[float | None, int | None, str | None]:
"""Kurs und Gegenbetrag, wie die Bank sie mitteilt.
Die Feldnamen unterscheiden sich je Anbindung, deshalb werden mehrere
Formen abgeklopft – und es wird still NICHTS gesetzt, wenn keine passt.
Raten waere hier schlimmer als die EZB-Naeherung, denn ein falscher Kurs
sieht aus wie ein richtiger.
"""
wechsel = umsatz.get("currency_exchange")
if isinstance(wechsel, list):
wechsel = next((w for w in wechsel if isinstance(w, dict)), None)
if not isinstance(wechsel, dict):
wechsel = {}
kurs = None
for feld in ("exchange_rate", "rate", "unit_rate"):
roh = wechsel.get(feld) if wechsel else None
if roh is None:
roh = umsatz.get(feld)
if roh is None:
continue
try:
wert = float(str(roh).replace(",", "."))
except (TypeError, ValueError):
continue
if wert > 0:
kurs = wert
break
# Der Betrag in der ANDEREN Waehrung. Nur brauchbar, wenn er sich von der
# Waehrung des Umsatzes unterscheidet – sonst ist es derselbe Betrag.
gegen = None
for quelle in (wechsel, umsatz):
for feld in ("instructed_amount", "original_amount", "source_amount"):
treffer = _betrag_mit_waehrung((quelle or {}).get(feld))
if treffer and treffer[1] != (waehrung or "").upper():
gegen = treffer
break
if gegen:
break
return kurs, (gegen[0] if gegen else None), (gegen[1] if gegen else None)
def _text(umsatz: dict, ausgabe: bool) -> tuple[str | None, str | None]:
"""Empfänger und Verwendungszweck. Bei einer Ausgabe zählt der Empfänger,
bei einer Einnahme der Absender."""
@@ -530,6 +585,9 @@ def hole_umsaetze(db: Session, user: User, verbindung: BankConnection,
if zeile.get("is_duplicate"):
continue
waehrung = ((umsatz.get("transaction_amount") or {}).get("currency")
or konto.currency)
kurs, gegen_betrag, gegen_waehrung = _umrechnung(umsatz, waehrung)
eintrag = InboxItem(
owner_id=user.id,
source=InboxSource.bank,
@@ -537,12 +595,14 @@ def hole_umsaetze(db: Session, user: User, verbindung: BankConnection,
account_id=konto.id,
date=tag,
amount_cents=cents,
currency=(umsatz.get("transaction_amount") or {}).get("currency")
or konto.currency,
currency=waehrung,
payee=payee,
note=zweck,
category_id=zeile.get("category_id"),
external_id=kennung,
fx_rate=kurs,
counter_amount_cents=gegen_betrag,
counter_currency=gegen_waehrung,
)
db.add(eintrag)
db.flush()