Files
certain/backend/tests/test_format_erkennung.py
T
GuidoandClaude Opus 5 e6801b0474 feat: CSV-Format automatisch erkennen
Bisher musste man bei jedem Import Trennzeichen, Datumsformat und
Spaltenzuordnung von Hand eintragen – die haeufigste Fehlerquelle ueberhaupt:
Ein falsches Datumsformat liess alle Zeilen durchfallen, ein falscher
Dezimaltrenner verzehnfachte Betraege.

Zuerst wird gegen die Bank-Vorlagen geprueft (Kopfzeilen-Signatur); alle vier
echten Ausaege des Nutzers werden so sicher erkannt. Sonst inhaltsbasiert:
welche Spalte enthaelt Daten, welche Betraege, welche den laengsten Text.

Zwei Fallen, die dabei aufgefallen sind und getestet werden:
- Deutsche Datumsangaben stecken voller Punkte, und viele Auszuege fuehren
  Buchungs- UND Valutadatum. Ohne sie herauszurechnen kaeme "." als
  Dezimalzeichen heraus und aus 1.204,70 wuerden 1.20.
- parse_date ist absichtlich nachsichtig; beim Erkennen darf es das nicht
  sein, sonst gewinnt ein Format mit Uhrzeit gegen das richtige ohne.

Ganzzahlen ohne Vorzeichen zaehlen nur halb, damit Referenznummern und Kurse
nicht als Betragsspalte durchgehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:17:37 +02:00

83 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Automatische Formaterkennung für CSV-Auszüge.
Bisher musste man bei jedem Import Trennzeichen, Datumsformat und
Spaltenzuordnung von Hand eintragen. Das ist die häufigste Fehlerquelle
gewesen: Ein falsches Datumsformat liess alle Zeilen durchfallen, ein
falscher Dezimaltrenner verzehnfachte Beträge.
"""
from app.services.csv_import import erkenne_format, parse_amount
def _erkenne(text: str) -> dict:
return erkenne_format(text.encode("utf-8"))
DEUTSCH = (
"Buchungstag;Wert;Auftraggeber/Empfänger;Verwendungszweck;Umsatz;Saldo\n"
"15.03.2026;15.03.2026;REWE Markt GmbH;Einkauf Filiale 42;-52,30;1.204,70\n"
"18.03.2026;18.03.2026;Arbeitgeber AG;Gehalt März;3.000,00;4.204,70\n"
"20.03.2026;20.03.2026;Stadtwerke;Abschlag Strom;-89,00;4.115,70\n"
)
def test_unbekannte_bank_wird_inhaltsbasiert_erkannt():
r = _erkenne(DEUTSCH)
assert r["ok"] is True
c = r["config"]
assert c["delimiter"] == ";"
assert c["date_format"] == "%d.%m.%Y"
assert c["column_map"]["date"] == 0
assert c["column_map"]["amount"] == 4
assert c["column_map"]["payee"] == 2
assert c["column_map"]["purpose"] == 3
assert c["column_map"]["balance"] == 5
def test_datumsspalten_verfaelschen_den_dezimaltrenner_nicht():
"""„15.03.2026“ steckt voller Punkte – und der Auszug führt zwei solche
Spalten. Ohne sie herauszurechnen käme „.“ als Dezimalzeichen heraus und
aus 1.204,70 würden 1.20 CHF."""
c = _erkenne(DEUTSCH)["config"]
assert c["decimal_sep"] == ","
assert parse_amount("1.204,70", c["decimal_sep"]) == 120470
assert parse_amount("-52,30", c["decimal_sep"]) == -5230
def test_datumsformat_ohne_uhrzeit_gewinnt_wenn_keine_da_ist():
"""parse_date ist absichtlich nachsichtig; beim Erkennen darf es das nicht
sein, sonst steht hinterher ein Format mit Uhrzeit in den Einstellungen."""
c = _erkenne("Tag;Betrag\n01.02.2026;-10,00\n02.02.2026;-20,00\n")["config"]
assert c["date_format"] == "%d.%m.%Y"
def test_englisch_mit_tabulator():
r = _erkenne("Date\tDescription\tAmount\tBalance\n"
"2026-03-15\tCOFFEE SHOP\t-4.50\t120.10\n"
"2026-03-16\tSALARY\t2500.00\t2620.10\n")
c = r["config"]
assert c["delimiter"] == "\t"
assert c["decimal_sep"] == "."
assert c["date_format"] == "%Y-%m-%d"
assert c["column_map"]["payee"] == 1
def test_kurs_und_referenzspalten_werden_nicht_fuer_betraege_gehalten():
"""Ganzzahlen ohne Vorzeichen sind verdächtig: Eine Referenznummer als
Betrag zu nehmen wäre ein stiller, teurer Fehler."""
r = _erkenne("Datum;Referenz;Empfänger;Betrag\n"
"01.03.2026;998877;Coop;-12,50\n"
"02.03.2026;998878;Migros;-8,20\n")
assert r["config"]["column_map"]["amount"] == 3
def test_datei_ohne_datumsspalte_wird_ehrlich_abgelehnt():
r = _erkenne("Name;Wert\nAlpha;12,00\nBeta;9,00\n")
assert r["ok"] is False
assert "Datum" in r["grund"]
def test_leere_datei():
assert _erkenne("")["ok"] is False
assert _erkenne("Nur;Kopfzeile\n")["ok"] is False