diff --git a/invoice_logic.py b/invoice_logic.py index 4b7e953..07b5cbb 100644 --- a/invoice_logic.py +++ b/invoice_logic.py @@ -147,7 +147,7 @@ def build_payment_terms_note(data): ANGEBOT_PATTERN = re.compile( - r"Angebot\s+([A-Za-z0-9][A-Za-z0-9\-/.]*)\s+vom\s+(\d{1,2}[.\-/]\d{1,2}[.\-/]\d{2,4})", + r"\bAngebot\s+([A-Za-z0-9][A-Za-z0-9\-/.]*)\s+vom\s+(\d{1,2}[.\-/]\d{1,2}[.\-/]\d{2,4})", re.IGNORECASE, ) @@ -162,6 +162,43 @@ def extract_angebot_reference(text): return f"Angebot {m.group(1)} vom {m.group(2)}" +# Referenzzeilen, die ein eigenes EN16931-Feld haben und daher NICHT in den Vorlauf-Freitext +# gehoeren (Auftragsbestaetigung BT-14, Angebot/Vergabeangebot, Lieferschein BT-16, Vertrag BT-12). +_REFERENCE_LINE_PATTERN = re.compile( + r"^(AUFTRAGSBESTÄTIGUNG|VERGABEANGEBOT|ANGEBOT|AUSSCHREIBUNG|LIEFERSCHEIN|VERTRAG|BESTELLUNG)\b", + re.IGNORECASE, +) +# Positionsnummer: alleinstehend "1" / "12" oder gegliedert "2.1". Markiert das Ende des Vorlaufs. +_POSITION_NUMBER_PATTERN = re.compile(r"^\d{1,3}(?:\.\d{1,3})?$") + + +def extract_pre_position_freetext(text): + """Liest den beschreibenden Vorlauftext zwischen dem Tabellenkopf ('Pos.') und der ersten + Position deterministisch aus dem PDF-Text - unabhaengig vom (oft unvollstaendigen) LLM. Alles + zwischen der ersten 'Pos.'-Zeile und der ersten Positionsnummer wird uebernommen, ausser den + Referenzzeilen (Auftragsbestaetigung/Angebot/Vergabeangebot/Lieferschein/Vertrag), die eigene + Felder haben. Gibt den Freitext zurueck ("" wenn keiner vorhanden) oder None, wenn die + Tabellenstruktur nicht erkennbar ist (dann bleibt ein evtl. LLM-Freitext unveraendert).""" + lines = [ln.strip() for ln in text.split("\n")] + + header_idx = next((i for i, ln in enumerate(lines) if ln == "Pos."), None) + if header_idx is None: + return None + + first_pos_idx = next( + (i for i in range(header_idx + 1, len(lines)) if _POSITION_NUMBER_PATTERN.match(lines[i])), + None, + ) + if first_pos_idx is None: + return None + + freetext_lines = [ + ln for ln in lines[header_idx + 1:first_pos_idx] + if ln and not _REFERENCE_LINE_PATTERN.match(ln) + ] + return "\n".join(freetext_lines) + + def sanitize_filename_part(value): """Macht einen String dateisystem-sicher: entfernt in Dateinamen kritische Zeichen (Pfadtrenner/Sonderzeichen) und fasst Whitespace zu einem Bindestrich zusammen.""" diff --git a/main.py b/main.py index 275e817..a4dc198 100644 --- a/main.py +++ b/main.py @@ -31,6 +31,7 @@ from invoice_logic import ( detect_contract_type, extract_hardware_serial_numbers, extract_angebot_reference, + extract_pre_position_freetext, ) # ========================================== @@ -234,6 +235,14 @@ with col2: final_data["contract_type"] = detect_contract_type(pdf_text) final_data["hardware_serials"] = extract_hardware_serial_numbers(pdf_text) + # Vorlauftext (BT-22) deterministisch aus dem PDF-Text lesen statt vom (oft + # unvollstaendigen) LLM: alles zwischen Tabellenkopf und erster Position, ohne die + # Referenzzeilen. Nur ersetzen, wenn die Tabellenstruktur erkannt wurde (sonst + # bleibt der LLM-Freitext als Fallback erhalten). + _freetext = extract_pre_position_freetext(pdf_text) + if _freetext is not None: + final_data["invoice_note"] = _freetext + # Angebotsbezug ("Angebot vom ") hat kein eigenes EN16931-Feld - als # Info fuer den Kunden in den Freitext (BT-22). Zusaetzlich: falls das LLM die # Angebotsnummer faelschlich in Vertrag (BT-12) oder Vergabeangebot (BT-17) gepackt