153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
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<date>\d{1,2}[./-]\d{1,2}[./-]\d{2,4}|\d{4}-\d{2}-\d{2})"
|
|
)
|
|
GENERIC_DATE_RE = re.compile(
|
|
r"\b(?P<date>\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",
|
|
)
|