feat: Kreditkarten-Auszuege lesen sich von selbst richtig herum

Auf einem Kartenauszug ist +6,00 eine AUSGABE und -10,00 die Zahlung der
Rechnung. Certain kannte diese Leserichtung nicht. Der scheinbare Ausweg
"Ein Betrag (immer Ausgabe)" machte auch die Rechnungszahlung und jede
Gutschrift zur Ausgabe.

Neuer Betragsmodus "signed_inverted". Er dreht Betrag UND Saldospalte:
Ein Auszug, der einen Einkauf als +6 fuehrt, schreibt auch den Saldo so –
die Schuld steigt. Wer nur den Betrag dreht, bekommt bei jeder einzelnen
Zeile eine Abweichung gemeldet, und die einzige Kontrollinstanz des
Imports wird wertlos. Dazu gibt es jetzt einen Test.

Waehlen muss den Modus niemand: /imports/detect erkennt jetzt auch das
KONTO – erkenne_konto konnte das laengst, benutzt hat es nur der
Telefon-Kurzbefehl. Ist das erkannte Konto eine Kreditkarte, wird die
Leserichtung mitgesetzt. Eng gefasst: nur bei einer Betragsspalte, nur
aus "signed" heraus, nur bei sicher erkanntem Konto. Und nie still – in
der Pruefansicht steht, dass umgekehrt gelesen wird, und wo man es
aendert.

Drei Dinge, die dabei auffielen:

- Der Abgleich rechnete mit dem Vorzeichen, gebucht wird aber nach Art
  (abs(Betrag) + Ausgabe/Einnahme). Wer eine Zeile umstellte, pruefte
  eine Buchung, die so nie entsteht.
- Dieselbe Luecke in der Vorschau: Die Betragsspalte faerbte nach dem
  Rohvorzeichen. Ein Umstellen aenderte sichtbar nichts.
- Auf Kartenauszuegen heisst die Empfaengerspalte "Haendler", und viele
  fuehren die eigene Kontonummer in jeder Zeile mit. Die gewann die
  Spaltenwahl ueber die Textlaenge – heraus kam eine Liste, in der bei
  jeder Buchung dasselbe stand.

Neu ausserdem eine Summenzeile je Richtung und eine Sammelaktion: Ist
eine Datei durchgehend verkehrt herum gelesen, sind alle Zeilen
betroffen, und bei 150 Positionen ist Einzelkorrektur keine Moeglichkeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-30 20:44:06 +02:00
co-authored by Claude Opus 5
parent 38697f828e
commit 98760a4652
7 changed files with 608 additions and 20 deletions
+59 -4
View File
@@ -9,7 +9,8 @@ from ..database import get_db
from ..deps import get_current_user, get_owned
from fastapi.responses import Response
from ..models import PendingUpload, Account, AmountMode, ImportBatch, ImportStatus, ImportTemplate, User
from ..models import (PendingUpload, Account, AccountType, AmountMode, ImportBatch,
ImportStatus, ImportTemplate, User)
from ..schemas import (
ImportBatchOut,
ImportCommitIn,
@@ -94,11 +95,65 @@ def _transient(user_id: int, **fields) -> ImportTemplate:
async def detect_format(
file: UploadFile = File(...),
encoding: str = Form("utf-8"),
_: User = Depends(get_current_user),
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> dict:
"""Format der Datei erkennen, damit niemand mehr Spalten von Hand zuordnet."""
"""Format UND Konto der Datei erkennen.
Das Konto gehoerte von Anfang an hierher: Es ist die gefaehrlichste
Entscheidung des ganzen Imports – ein Auszug im falschen Konto verfaelscht
zwei Salden auf einmal – und ``erkenne_konto`` konnte sie laengst. Benutzt
hat die Funktion bisher nur der Telefon-Kurzbefehl.
``erkenne_konto`` raet nicht: Bleibt es mehrdeutig, kommt ``None`` zurueck
und der Nutzer waehlt wie bisher.
"""
daten = await file.read()
return csv_import.erkenne_format(daten, encoding)
erkannt = csv_import.erkenne_format(daten, encoding)
konto = csv_import.erkenne_konto(db, user, daten, erkannt)
erkannt["konto"] = konto
acc_id = konto.get("account_id")
if acc_id:
acc = db.get(Account, acc_id)
if acc is not None and acc.owner_id == user.id:
_kreditkarten_regel(erkannt, acc)
return erkannt
def _kreditkarten_regel(erkannt: dict, acc: Account) -> None:
"""Bei einer Kreditkarte ist + eine Ausgabe.
Die Regel steht serverseitig und an EINER Stelle. Im Frontend gebaut
zerbraeche sie an der Reihenfolge: ``setAccountId`` und die Formaterkennung
laufen dort im selben Durchgang, die Regel laese also noch den alten
Zustand.
Eng gefasst, mit Absicht:
- Nur ``signed``. Bei getrennten Soll-/Haben-Spalten benennen die Spalten
die Richtung selbst, und ``abs_expense`` wurde bewusst gewaehlt.
- Nur mit einer Betragsspalte.
- Nur, wenn das Konto sicher erkannt wurde – sonst raet man zweimal.
Umgeschaltet, nicht erzwungen: Der Modus steht unter „Format" und laesst
sich aendern. Und in der Pruefansicht steht, dass umgekehrt gelesen wird –
eine stille Umkehr waere schlimmer als gar keine.
"""
if acc.type != AccountType.kreditkarte or not erkannt.get("ok"):
return
config = erkannt.get("config") or {}
if config.get("amount_mode") != AmountMode.signed.value:
return
if (config.get("column_map") or {}).get("amount") is None:
return
config["amount_mode"] = AmountMode.signed_inverted.value
erkannt["kreditkarte"] = True
erkannt["begruendung"] = (
(erkannt.get("begruendung") or "")
+ f" · Kreditkarte „{acc.name}“: + im Auszug = Ausgabe"
).strip(" ·")
@router.post("/raw")