feat: clear data panel on new upload; deterministic doc-reference routing (Vergabeangebot->BT-17)

- Rechts angezeigte Rechnungsdaten werden verworfen, sobald eine neue/andere PDF geladen wird
  (nicht mehr die vorige Rechnung bis zum erneuten Extrahieren).
- extract_document_references(): Auftragsbestaetigung->BT-14, Lieferschein->BT-16,
  Vergabeangebot->BT-17 deterministisch aus den Referenzzeilen; korrigiert LLM-Fehlzuordnung
  (Vergabeangebot landete faelschlich in BT-12 Vertrag). Verifiziert gegen 7 echte Rechnungen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dfuhrmann3 2026-07-13 14:49:35 +02:00
parent 3832d5cdf2
commit f92c4ffac6
2 changed files with 41 additions and 0 deletions

View File

@ -199,6 +199,28 @@ def extract_pre_position_freetext(text):
return "\n".join(freetext_lines) return "\n".join(freetext_lines)
# Beleg-Referenzen aus den Referenzzeilen: Schluesselwort -> Feld. Zuverlaessiger als die
# LLM-Zuordnung, die z.B. ein Vergabeangebot faelschlich in BT-12 (Vertrag) statt BT-17 einsortiert.
_DOC_REF_PATTERNS = {
"order_id": re.compile(r"\bAUFTRAGSBESTÄTIGUNG\s+([A-Za-z0-9][A-Za-z0-9\-/.]*)", re.IGNORECASE),
"delivery_note_id": re.compile(r"\bLIEFERSCHEIN\s+([A-Za-z0-9][A-Za-z0-9\-/.]*)", re.IGNORECASE),
"tender_id": re.compile(r"\bVERGABEANGEBOT\s+([A-Za-z0-9][A-Za-z0-9\-/.]*)", re.IGNORECASE),
}
def extract_document_references(text):
"""Liest Beleg-Referenzen deterministisch aus den Referenzzeilen des PDF-Texts:
Auftragsbestaetigung (BT-14), Lieferschein (BT-16), Vergabeangebot (BT-17). Gibt ein Dict nur
mit den gefundenen Feldern zurueck - so lassen sich die (teils falschen) LLM-Zuordnungen gezielt
korrigieren, ohne fehlende Felder zu ueberschreiben."""
result = {}
for field, pattern in _DOC_REF_PATTERNS.items():
m = pattern.search(text)
if m:
result[field] = m.group(1).strip()
return result
def sanitize_filename_part(value): def sanitize_filename_part(value):
"""Macht einen String dateisystem-sicher: entfernt in Dateinamen kritische Zeichen """Macht einen String dateisystem-sicher: entfernt in Dateinamen kritische Zeichen
(Pfadtrenner/Sonderzeichen) und fasst Whitespace zu einem Bindestrich zusammen.""" (Pfadtrenner/Sonderzeichen) und fasst Whitespace zu einem Bindestrich zusammen."""

19
main.py
View File

@ -32,6 +32,7 @@ from invoice_logic import (
extract_hardware_serial_numbers, extract_hardware_serial_numbers,
extract_angebot_reference, extract_angebot_reference,
extract_pre_position_freetext, extract_pre_position_freetext,
extract_document_references,
) )
# ========================================== # ==========================================
@ -147,6 +148,14 @@ with col1:
st.subheader("1. Dokument hochladen") st.subheader("1. Dokument hochladen")
uploaded_file = st.file_uploader("Wähle eine Rechnungs-PDF", type=["pdf"]) uploaded_file = st.file_uploader("Wähle eine Rechnungs-PDF", type=["pdf"])
pdf_text = "" pdf_text = ""
# Neue/andere Datei (oder Datei entfernt)? -> zuvor extrahierte Daten verwerfen, damit rechts
# nicht die vorige Rechnung stehen bleibt, bis erneut extrahiert wird.
_current_file_id = f"{uploaded_file.name}:{uploaded_file.size}" if uploaded_file is not None else None
if st.session_state.get("_last_file_id") != _current_file_id:
st.session_state._last_file_id = _current_file_id
st.session_state.extracted_data = None
if uploaded_file is not None: if uploaded_file is not None:
doc = fitz.open(stream=uploaded_file.read(), filetype="pdf") doc = fitz.open(stream=uploaded_file.read(), filetype="pdf")
for page in doc: for page in doc:
@ -243,6 +252,16 @@ with col2:
if _freetext is not None: if _freetext is not None:
final_data["invoice_note"] = _freetext final_data["invoice_note"] = _freetext
# Beleg-Referenzen deterministisch aus den Referenzzeilen lesen und die (teils falsch
# zugeordneten) LLM-Werte korrigieren: Auftragsbestaetigung -> BT-14, Lieferschein ->
# BT-16, Vergabeangebot -> BT-17. Nur gefundene Felder ueberschreiben.
_doc_refs = extract_document_references(pdf_text)
for _field, _val in _doc_refs.items():
final_data[_field] = _val
# Falls das LLM die Vergabeangebot-Nummer faelschlich in BT-12 (Vertrag) hatte:
if _doc_refs.get("tender_id") and str(final_data.get("contract_id", "")).strip() == _doc_refs["tender_id"]:
final_data["contract_id"] = ""
# Angebotsbezug ("Angebot <Nr> vom <Datum>") hat kein eigenes EN16931-Feld - als # Angebotsbezug ("Angebot <Nr> vom <Datum>") hat kein eigenes EN16931-Feld - als
# Info fuer den Kunden in den Freitext (BT-22). Zusaetzlich: falls das LLM die # 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 # Angebotsnummer faelschlich in Vertrag (BT-12) oder Vergabeangebot (BT-17) gepackt