diff --git a/backend/app/services/mail_parse.py b/backend/app/services/mail_parse.py index 4040bd8..ce52122 100644 --- a/backend/app/services/mail_parse.py +++ b/backend/app/services/mail_parse.py @@ -30,10 +30,9 @@ from decimal import Decimal, InvalidOperation # Beschriftungen je Feld, längere zuerst (spezifischer schlägt allgemeiner). LABELS_BETRAG = ("betrag", "gesendet", "empfangen", "erhalten", "amount", "summe") -LABELS_EMPFAENGER = ("begünstigter", "beguenstigter", "empfänger", "empfaenger", - "zahlungsempfänger", "an", "händler", "haendler", "to") -LABELS_DATUM = ("ausführungsdatum", "ausfuehrungsdatum", "valutadatum", "buchungsdatum", - "datum", "am", "date") +LABELS_EMPFAENGER = ("beguenstigter", "zahlungsempfaenger", "empfaenger", "haendler", + "beguenstigte", "an", "to") +LABELS_DATUM = ("ausfuehrungsdatum", "valutadatum", "buchungsdatum", "datum", "am", "date") LABELS_KONTO = ("konto", "account", "iban") LABELS_STATUS = ("neuer status", "status") @@ -100,6 +99,20 @@ def html_zu_text(roh: str) -> str: return "\n".join(z for z in zeilen if z) +def _schluessel(text: str) -> str: + """Beschriftung auf eine vergleichbare Form bringen. + + Umlaute verschriftlichen und alles ausser Buchstaben und Ziffern + wegwerfen. „Begünstigter", „Beguenstigter" und „BEGÜNSTIGTER :" ergeben + damit denselben Schlüssel – sonst haengt die Erkennung daran, wie das + Mailprogramm den Umlaut kodiert hat. + """ + t = text.strip().lower() + for von, nach in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")): + t = t.replace(von, nach) + return re.sub(r"[^a-z0-9]", "", t) + + def _zahl_zu_cent(roh: str) -> int | None: s = roh.replace("'", "").replace("’", "").replace(" ", "") if "," in s and "." in s: @@ -134,7 +147,9 @@ def _felder(text: str) -> dict[str, list[tuple[str, int, str]]]: ``Gesendet`` / ``324 €`` den Inhalt der Folgezeile. """ gefunden: dict[str, list[tuple[str, int, str]]] = {} - zeilen = text.splitlines() + # Weitergeleitete Mails kommen oft mit „> " zitiert an; die Beschriftung + # steht dann nicht mehr am Zeilenanfang. + zeilen = [re.sub(r"^[\s>]+", "", z) for z in text.splitlines()] pos = 0 for i, zeile in enumerate(zeilen): start = pos @@ -142,12 +157,12 @@ def _felder(text: str) -> dict[str, list[tuple[str, int, str]]]: if ":" in zeile: links, rechts = zeile.split(":", 1) beschriftung = links.strip() - schl, wert = beschriftung.lower(), rechts.strip() + schl, wert = _schluessel(beschriftung), rechts.strip() if wert: gefunden.setdefault(schl, []).append((wert, start, beschriftung)) continue beschriftung = zeile.strip().rstrip(":") - schl = beschriftung.lower() + schl = _schluessel(beschriftung) if schl and len(schl) <= 30 and i + 1 < len(zeilen): folge = zeilen[i + 1].strip() if folge and ":" not in folge: @@ -156,8 +171,14 @@ def _felder(text: str) -> dict[str, list[tuple[str, int, str]]]: def _erster_treffer(felder: dict, labels: tuple[str, ...]) -> tuple[str, int, str] | None: - """Wert, Stelle und die Beschriftung in Originalschreibweise.""" - for label in labels: + """Wert, Stelle und die Beschriftung in Originalschreibweise. + + Die Suchbegriffe laufen durch dieselbe Normalisierung wie die gefundenen + Beschriftungen – sonst muesste man sie doppelt pflegen, einmal lesbar und + einmal fuer den Vergleich. + """ + for roh in labels: + label = _schluessel(roh) if label in felder: wert, stelle, beschriftung = felder[label][0] return wert, stelle, beschriftung diff --git a/backend/tests/test_mail_parse.py b/backend/tests/test_mail_parse.py index b4c7d6b..760dfec 100644 --- a/backend/tests/test_mail_parse.py +++ b/backend/tests/test_mail_parse.py @@ -103,3 +103,20 @@ def test_blosse_zahlen_sind_keine_betraege(): e = mail_parse.parse("Auftrag 12345", "Referenz 998877\nBestellnummer 4711\n") assert e.betrag is None assert "betrag" in e.unklar + + +def test_beschriftung_wird_unabhaengig_von_umlaut_und_zitat_erkannt(): + """Weitergeleitete Mails kommen zitiert an, und Umlaute schreibt nicht jedes + Programm gleich. „Begünstigter“ muss trotzdem greifen – ohne den + Begünstigten ist ein Eintrag im Eingang wertlos.""" + varianten = [ + " Begünstigter: Sunrise GmbH / Yallo\n Betrag: 14.90 CHF\n", + "> Begünstigter: Sunrise GmbH / Yallo\n> Betrag: 14.90 CHF\n", + ">> BEGÜNSTIGTER : Sunrise GmbH / Yallo\n>> Betrag: 14.90 CHF\n", + " Beguenstigter: Sunrise GmbH / Yallo\n Betrag: 14.90 CHF\n", + ] + for text in varianten: + e = mail_parse.parse("Zahlungsauftrag", text) + assert e.empfaenger is not None, f"kein Empfänger erkannt in: {text!r}" + assert e.empfaenger.wert == "Sunrise GmbH / Yallo" + assert e.betrag.wert == 1490