feat: Konto wird aus der Datei erkannt – ein Kurzbefehl genuegt

Bisher brauchte es je Konto einen eigenen Kurzbefehl, weil account_id
Pflicht war. Jetzt ist sie optional: Raiffeisen fuehrt die eigene IBAN in der
Datei mit (Treffer gegen die Erkennungs-Texte der Konten), Revolut die
Waehrung – zusammen mit der erkannten Bank und dem Import-Verlauf reicht das.

Bleibt es mehrdeutig, wird NICHT geraten: Ein Auszug im falschen Konto
verfaelscht zwei Salden auf einmal. Stattdessen nennt die Fehlermeldung die
Konten samt Nummern, sodass man sie im Kurzbefehl nachtragen kann. Hat der
Schluessel ein Standardkonto, gilt das.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-22 23:54:27 +02:00
co-authored by Claude Opus 5
parent 1b401ff358
commit e8a862bd63
5 changed files with 161 additions and 14 deletions
+77
View File
@@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
from ..models import (
Account,
AccountIdentifier,
InboxItem,
InboxStatus,
AmountMode,
@@ -273,6 +274,82 @@ def erkenne_format(data: bytes, encoding: str = "utf-8") -> dict:
}
def erkenne_konto(db: Session, user: User, data: bytes, erkannt: dict) -> dict:
"""Zu welchem Konto gehört diese Datei?
Damit reicht EIN Kurzbefehl fürs Telefon statt einer je Konto. Zwei
Anhaltspunkte, in dieser Reihenfolge:
1. Eine Spalte mit der EIGENEN IBAN (Raiffeisen führt sie in Spalte 0).
Trifft sie einen Erkennungs-Text eines Kontos, ist die Sache eindeutig.
2. Die Währung der Zeilen zusammen mit der erkannten Bank. Revolut liefert
je Währung eine eigene Datei; welches Konto gemeint ist, verrät der
Import-Verlauf – dorthin ging zuletzt eine Datei derselben Bank.
Bleibt es mehrdeutig, wird NICHT geraten: Ein Auszug im falschen Konto
verfälscht zwei Salden auf einmal.
"""
if not erkannt.get("ok"):
return {"account_id": None, "grund": erkannt.get("grund", "Format unbekannt")}
config = erkannt["config"]
text = decode_bytes(data, config.get("encoding") or "utf-8")
zeilen = [r for r in csv.reader(io.StringIO(text), delimiter=config["delimiter"])
if any((c or "").strip() for c in r)]
if len(zeilen) < 2:
return {"account_id": None, "grund": "Zu wenige Zeilen"}
proben = zeilen[1:31]
konten = db.query(Account).filter(Account.owner_id == user.id,
Account.is_archived.is_(False)).all()
# 1. Eigene IBAN in einer Spalte.
idents: dict[str, int] = {}
for acc_id, wert in db.query(AccountIdentifier.account_id, AccountIdentifier.value_norm).join(
Account, Account.id == AccountIdentifier.account_id
).filter(Account.owner_id == user.id):
if wert:
idents[normalize_iban(wert)] = acc_id
breite = max(len(r) for r in proben)
for i in range(breite):
werte = {normalize_iban(r[i]) for r in proben if i < len(r) and r[i].strip()}
werte = {w for w in werte if len(w) >= 15}
if len(werte) == 1:
treffer = idents.get(next(iter(werte)))
if treffer:
return {"account_id": treffer, "grund": "eigene IBAN in der Datei",
"sicher": True}
# 2. Währung plus zuletzt benutztes Konto derselben Bank.
waehrung_i = config["column_map"].get("currency")
waehrungen = set()
if waehrung_i is not None:
waehrungen = {r[waehrung_i].strip().upper()
for r in proben if waehrung_i < len(r) and r[waehrung_i].strip()}
if len(waehrungen) == 1:
w = next(iter(waehrungen))
passend = [a for a in konten if a.currency.upper() == w]
if len(passend) == 1:
return {"account_id": passend[0].id, "grund": f"einziges Konto in {w}", "sicher": True}
if passend and erkannt.get("preset"):
frueher = {
b.account_id
for b in db.query(ImportBatch).filter(
ImportBatch.owner_id == user.id,
ImportBatch.preset == erkannt["preset"],
ImportBatch.status == ImportStatus.committed,
)
}
treffer = [a for a in passend if a.id in frueher]
if len(treffer) == 1:
return {"account_id": treffer[0].id,
"grund": f"{w} und zuletzt für „{erkannt['preset']}“ benutzt",
"sicher": True}
return {"account_id": None,
"grund": "Das Konto lässt sich aus der Datei nicht sicher ableiten"}
# --- Kleine Parser-Helfer --------------------------------------------------