from __future__ import annotations import re from datetime import date from orc_renaming.config import AppConfig from orc_renaming.models import DocumentScope, DocumentType, ExtractionResult INVOICE_TERMS = ( "rechnung", "rechnungsnummer", "rechnungsbetrag", "nettobetrag", "bruttobetrag", "umsatzsteuer", "mehrwertsteuer", "zahlbetrag", ) LABELED_DATE_RE = re.compile( r"(?i)\b(?:rechnungsdatum|belegdatum|briefdatum|datum)\s*[:\-]?\s*" r"(?P\d{1,2}[./-]\d{1,2}[./-]\d{2,4}|\d{4}-\d{2}-\d{2})" ) GENERIC_DATE_RE = re.compile( r"\b(?P\d{1,2}[./-]\d{1,2}[./-]\d{4}|\d{4}-\d{2}-\d{2})\b" ) def _normalize_search_text(value: str) -> str: return re.sub(r"\s+", " ", value).casefold() def _parse_german_date(value: str) -> date | None: if re.fullmatch(r"\d{4}-\d{2}-\d{2}", value): try: return date.fromisoformat(value) except ValueError: return None parts = re.split(r"[./-]", value) if len(parts) != 3: return None day, month, year = (int(part) for part in parts) if year < 100: year += 2000 try: return date(year, month, day) except ValueError: return None def _extract_date(text: str) -> tuple[date | None, str | None, bool]: for match in LABELED_DATE_RE.finditer(text): result = _parse_german_date(match.group("date")) if result: return result, match.group(0), True for match in GENERIC_DATE_RE.finditer(text): result = _parse_german_date(match.group("date")) if result: return result, match.group(0), False return None, None, False def extract_with_rules(text: str, config: AppConfig) -> ExtractionResult: search_text = _normalize_search_text(text) evidence: list[str] = [] warnings: list[str] = [] term_hits = sorted(term for term in INVOICE_TERMS if term in search_text) if len(term_hits) >= 2: document_type = DocumentType.INVOICE type_confidence = 0.93 evidence.append(f"Rechnungsmerkmale: {', '.join(term_hits[:5])}") elif len(term_hits) == 1: document_type = DocumentType.INVOICE type_confidence = 0.68 evidence.append(f"Rechnungsmerkmal: {term_hits[0]}") warnings.append("Dokumenttyp nur schwach erkannt") else: document_type = DocumentType.UNKNOWN type_confidence = 0.0 document_date, date_evidence, date_was_labeled = _extract_date(text) if date_evidence: evidence.append(f"Datum: {date_evidence}") if document_date and not date_was_labeled: warnings.append("Datum ohne eindeutige Feldbezeichnung erkannt") company: str | None = None for candidate in config.companies: aliases = [candidate.name, *candidate.aliases] if any(_normalize_search_text(alias) in search_text for alias in aliases): company = candidate.name evidence.append(f"Firma aus Stammdaten: {candidate.name}") break property_matches: list[tuple[str, str]] = [] for candidate in config.properties: hits = [ marker for marker in candidate.markers if _normalize_search_text(marker) in search_text ] if hits: property_matches.append((candidate.id, hits[0])) property_id: str | None = None scope = DocumentScope.UNKNOWN if len(property_matches) == 1: property_id, marker = property_matches[0] scope = DocumentScope.PROPERTY evidence.append(f"Immobilienmerkmal {property_id}: {marker}") elif len(property_matches) > 1: ids = ", ".join(item[0] for item in property_matches) warnings.append(f"Mehrere Immobilien passen: {ids}") if scope == DocumentScope.UNKNOWN: private_hits = [ marker for marker in config.private_markers if _normalize_search_text(marker) in search_text ] if private_hits: scope = DocumentScope.PRIVATE evidence.append(f"Privatmerkmal: {private_hits[0]}") elif ( document_type == DocumentType.INVOICE and config.processing.assume_unmatched_invoices_private ): scope = DocumentScope.PRIVATE warnings.append("Ohne Treffer gemäß Konfiguration als privat angenommen") confidence_parts = [type_confidence] if document_date: confidence_parts.append(0.95 if date_was_labeled else 0.65) if company: confidence_parts.append(0.98) if scope != DocumentScope.UNKNOWN: confidence_parts.append(0.98) confidence = min(confidence_parts) if confidence_parts else 0.0 return ExtractionResult( document_type=document_type, scope=scope, document_date=document_date, company=company, property_id=property_id, confidence=confidence, evidence=evidence, warnings=warnings, source="rules", )