""" Sorter Modul Regel-basierte Erkennung und Benennung von Dokumenten """ import re from pathlib import Path from datetime import datetime from typing import Dict, List, Optional, Any import logging import shutil from .extraktoren import extrahiere_alle_felder, baue_dateiname logger = logging.getLogger(__name__) class Sorter: """Sortiert und benennt Dokumente basierend auf Regeln""" def __init__(self, regeln: List[Dict]): """ Args: regeln: Liste von Regel-Dicts, sortiert nach Priorität """ # Nach Priorität sortieren (niedrig = wichtig) self.regeln = sorted(regeln, key=lambda r: r.get("prioritaet", 100)) def finde_passende_regel(self, dokument_info: Dict) -> Optional[Dict]: """ Findet die erste passende Regel für ein Dokument Args: dokument_info: Dict mit text, original_name, absender, etc. Returns: Passende Regel oder None """ for regel in self.regeln: if not regel.get("aktiv", True): continue muster = regel.get("muster", {}) if self._pruefe_muster(muster, dokument_info): logger.info(f"Regel '{regel.get('name')}' matched für {dokument_info.get('original_name')}") return regel return None def _pruefe_muster(self, muster: Dict, dokument_info: Dict) -> bool: """Prüft ob alle Muster auf das Dokument zutreffen""" text = dokument_info.get("text", "").lower() original_name = dokument_info.get("original_name", "").lower() absender = dokument_info.get("absender", "").lower() # keywords (einfache Komma-getrennte Liste - für UI) if "keywords" in muster: keywords = muster["keywords"] if isinstance(keywords, str): keywords = [k.strip() for k in keywords.split(",")] # Alle Keywords müssen vorkommen for keyword in keywords: keyword = keyword.lower().strip() if keyword and keyword not in text and keyword not in original_name: return False # absender_contains if "absender_contains" in muster: if muster["absender_contains"].lower() not in absender: return False # dateiname_match if "dateiname_match" in muster: pattern = muster["dateiname_match"] if isinstance(pattern, str): if pattern.lower() not in original_name: return False elif isinstance(pattern, list): if not any(p.lower() in original_name for p in pattern): return False # text_match (alle müssen enthalten sein) if "text_match" in muster: patterns = muster["text_match"] if isinstance(patterns, str): patterns = [patterns] for pattern in patterns: if pattern.lower() not in text: return False # text_match_any (mindestens einer muss enthalten sein) if "text_match_any" in muster: patterns = muster["text_match_any"] if isinstance(patterns, str): patterns = [patterns] if not any(p.lower() in text for p in patterns): return False # text_regex if "text_regex" in muster: pattern = muster["text_regex"] if not re.search(pattern, text, re.IGNORECASE): return False return True def extrahiere_felder(self, regel: Dict, dokument_info: Dict) -> Dict[str, Any]: """ Extrahiert Felder aus dem Dokument - nutzt globale Extraktoren mit Fallbacks Returns: Dict mit extrahierten Werten """ text = dokument_info.get("text", "") regel_extraktion = regel.get("extraktion", {}) # Globale Extraktoren nutzen (mit Regel-spezifischen Überschreibungen) felder = extrahiere_alle_felder(text, dokument_info, regel_extraktion) # Regel-spezifische statische Werte überschreiben for feld_name, feld_config in regel_extraktion.items(): if isinstance(feld_config, dict): if "wert" in feld_config: felder[feld_name] = feld_config["wert"] elif "regex" in feld_config: # Einzelnes Regex aus Regel wert = self._extrahiere_mit_regex(feld_config, text) if wert: felder[feld_name] = wert elif isinstance(feld_config, str): # Direkter statischer Wert felder[feld_name] = feld_config return felder def _extrahiere_mit_regex(self, config: Dict, text: str) -> Optional[str]: """Extrahiert ein Feld mit einem einzelnen Regex""" try: match = re.search(config["regex"], text, re.IGNORECASE | re.MULTILINE) if match: wert = match.group(1) if match.groups() else match.group(0) # Datum formatieren if "format" in config: try: datum = datetime.strptime(wert.strip(), config["format"]) return datum.strftime("%Y-%m-%d") except: pass # Betrag formatieren if config.get("typ") == "betrag": wert = self._formatiere_betrag(wert) return wert.strip() except Exception as e: logger.debug(f"Regex-Extraktion fehlgeschlagen: {e}") return None def _formatiere_betrag(self, betrag: str) -> str: """Formatiert Betrag einheitlich""" betrag = betrag.replace(" ", "").replace(".", "").replace(",", ".") try: wert = float(betrag) if wert == int(wert): return str(int(wert)) return f"{wert:.2f}".replace(".", ",") except: return betrag def generiere_dateinamen(self, regel: Dict, extrahierte_felder: Dict) -> str: """ Generiert den neuen Dateinamen basierend auf Schema Nutzt den intelligenten Schema-Builder der fehlende Felder entfernt """ schema = regel.get("schema", "{datum} - Dokument.pdf") return baue_dateiname(schema, extrahierte_felder, ".pdf") def verschiebe_datei(self, quell_pfad: str, ziel_ordner: str, neuer_name: str) -> str: """ Verschiebt und benennt Datei um Returns: Neuer Pfad der Datei """ ziel_dir = Path(ziel_ordner) ziel_dir.mkdir(parents=True, exist_ok=True) ziel_pfad = ziel_dir / neuer_name # Eindeutigen Namen sicherstellen counter = 1 original_name = ziel_pfad.stem suffix = ziel_pfad.suffix while ziel_pfad.exists(): ziel_pfad = ziel_dir / f"{original_name} ({counter}){suffix}" counter += 1 # Verschieben shutil.move(quell_pfad, ziel_pfad) logger.info(f"Verschoben: {quell_pfad} -> {ziel_pfad}") return str(ziel_pfad) # ============ STANDARD-DOKUMENTTYPEN ============ # Diese werden für das einfache UI verwendet DOKUMENTTYPEN = { "rechnung": { "name": "Rechnung", "keywords": ["rechnung", "invoice"], "schema": "{datum} - Rechnung - {firma} - {nummer} - {betrag} EUR.pdf", "unterordner": "rechnungen" }, "angebot": { "name": "Angebot", "keywords": ["angebot", "quotation", "offerte"], "schema": "{datum} - Angebot - {firma} - {nummer} - {betrag} EUR.pdf", "unterordner": "angebote" }, "gutschrift": { "name": "Gutschrift", "keywords": ["gutschrift", "credit"], "schema": "{datum} - Gutschrift - {firma} - {nummer} - {betrag} EUR.pdf", "unterordner": "gutschriften" }, "lieferschein": { "name": "Lieferschein", "keywords": ["lieferschein", "delivery"], "schema": "{datum} - Lieferschein - {firma} - {nummer}.pdf", "unterordner": "lieferscheine" }, "auftragsbestaetigung": { "name": "Auftragsbestätigung", "keywords": ["auftragsbestätigung", "bestellbestätigung"], "schema": "{datum} - Auftragsbestätigung - {firma} - {nummer}.pdf", "unterordner": "auftraege" }, "vertrag": { "name": "Vertrag", "keywords": ["vertrag", "contract"], "schema": "Vertrag - {firma} - {nummer} - {datum}.pdf", "unterordner": "vertraege" }, "versicherung": { "name": "Versicherung", "keywords": ["versicherung", "police", "beitrag"], "schema": "Versicherung - {firma} - {nummer} - {datum}.pdf", "unterordner": "versicherungen" }, "zeugnis": { "name": "Zeugnis", "keywords": ["zeugnis", "zertifikat"], "schema": "Zeugnis - {firma} - {nummer} - {datum}.pdf", "unterordner": "zeugnisse" }, "bescheinigung": { "name": "Bescheinigung", "keywords": ["bescheinigung", "nachweis", "bestätigung"], "schema": "Bescheinigung - {firma} - {nummer} - {datum}.pdf", "unterordner": "bescheinigungen" }, "kontoauszug": { "name": "Kontoauszug", "keywords": ["kontoauszug", "account statement"], "schema": "{datum} - Kontoauszug - {firma} - {nummer}.pdf", "unterordner": "kontoauszuege" }, "sonstiges": { "name": "Sonstiges", "keywords": [], "schema": "{datum} - {typ} - {firma}.pdf", "unterordner": "sonstiges" } } def erstelle_einfache_regel(name: str, dokumenttyp: str, keywords: str, firma_wert: str = None, unterordner: str = None, prioritaet: int = 50) -> Dict: """ Erstellt eine einfache Regel basierend auf Dokumenttyp Args: name: Name der Regel (z.B. "Sonepar Rechnung") dokumenttyp: Typ aus DOKUMENTTYPEN keywords: Komma-getrennte Keywords zur Erkennung firma_wert: Optionaler fester Firmenwert unterordner: Optionaler Unterordner (überschreibt Standard) prioritaet: Priorität (niedriger = wichtiger) Returns: Regel-Dict für die Datenbank """ typ_config = DOKUMENTTYPEN.get(dokumenttyp, DOKUMENTTYPEN["sonstiges"]) regel = { "name": name, "prioritaet": prioritaet, "aktiv": True, "muster": { "keywords": keywords }, "extraktion": {}, "schema": typ_config["schema"], "unterordner": unterordner or typ_config["unterordner"] } # Feste Firma wenn angegeben if firma_wert: regel["extraktion"]["firma"] = {"wert": firma_wert} return regel def liste_dokumenttypen() -> List[Dict]: """Gibt Liste aller Dokumenttypen für UI zurück""" return [ {"id": key, "name": config["name"], "schema": config["schema"]} for key, config in DOKUMENTTYPEN.items() ]