851 lines
26 KiB
Python
851 lines
26 KiB
Python
"""
|
|
API Routes - Getrennte Bereiche: Mail-Abruf und Datei-Sortierung
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Optional
|
|
from pydantic import BaseModel
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import json
|
|
import asyncio
|
|
|
|
from ..models.database import get_db, Postfach, QuellOrdner, SortierRegel, VerarbeiteteDatei, VerarbeiteteMail
|
|
from ..modules.mail_fetcher import MailFetcher
|
|
from ..modules.pdf_processor import PDFProcessor
|
|
from ..modules.sorter import Sorter
|
|
|
|
router = APIRouter(prefix="/api", tags=["api"])
|
|
|
|
|
|
# ============ Pydantic Models ============
|
|
|
|
class PostfachCreate(BaseModel):
|
|
name: str
|
|
imap_server: str
|
|
imap_port: int = 993
|
|
email: str
|
|
passwort: str
|
|
ordner: str = "INBOX"
|
|
alle_ordner: bool = False # Alle IMAP-Ordner durchsuchen
|
|
nur_ungelesen: bool = False # Nur ungelesene Mails (False = alle)
|
|
ziel_ordner: str
|
|
erlaubte_typen: List[str] = [".pdf"]
|
|
max_groesse_mb: int = 25
|
|
|
|
|
|
class PostfachResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
imap_server: str
|
|
email: str
|
|
ordner: str
|
|
alle_ordner: bool
|
|
nur_ungelesen: bool
|
|
ziel_ordner: str
|
|
erlaubte_typen: List[str]
|
|
max_groesse_mb: int
|
|
letzter_abruf: Optional[datetime]
|
|
letzte_anzahl: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class OrdnerCreate(BaseModel):
|
|
name: str
|
|
pfad: str
|
|
ziel_ordner: str
|
|
rekursiv: bool = True
|
|
dateitypen: List[str] = [".pdf", ".jpg", ".jpeg", ".png", ".tiff"]
|
|
|
|
|
|
class OrdnerResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
pfad: str
|
|
ziel_ordner: str
|
|
rekursiv: bool
|
|
dateitypen: List[str]
|
|
aktiv: bool
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class RegelCreate(BaseModel):
|
|
name: str
|
|
prioritaet: int = 100
|
|
muster: dict = {}
|
|
extraktion: dict = {}
|
|
schema: str = "{datum} - Dokument.pdf"
|
|
unterordner: Optional[str] = None
|
|
|
|
|
|
class RegelResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
prioritaet: int
|
|
aktiv: bool
|
|
muster: dict
|
|
extraktion: dict
|
|
schema: str
|
|
unterordner: Optional[str]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class RegelTestRequest(BaseModel):
|
|
regel: dict
|
|
text: str
|
|
|
|
|
|
# ============ Verzeichnis-Browser ============
|
|
|
|
@router.get("/browse")
|
|
def browse_directory(path: str = "/"):
|
|
"""Listet Verzeichnisse für File-Browser"""
|
|
import os
|
|
|
|
# Sicherheit: Nur bestimmte Basispfade erlauben
|
|
allowed_bases = ["/srv", "/home", "/mnt", "/media", "/data", "/tmp"]
|
|
path = os.path.abspath(path)
|
|
|
|
# Prüfen ob Pfad erlaubt
|
|
is_allowed = any(path.startswith(base) for base in allowed_bases) or path == "/"
|
|
if not is_allowed:
|
|
return {"error": "Pfad nicht erlaubt", "entries": []}
|
|
|
|
if not os.path.exists(path):
|
|
return {"error": "Pfad existiert nicht", "entries": []}
|
|
|
|
if not os.path.isdir(path):
|
|
return {"error": "Kein Verzeichnis", "entries": []}
|
|
|
|
try:
|
|
entries = []
|
|
for entry in sorted(os.listdir(path)):
|
|
full_path = os.path.join(path, entry)
|
|
if os.path.isdir(full_path):
|
|
entries.append({
|
|
"name": entry,
|
|
"path": full_path,
|
|
"type": "directory"
|
|
})
|
|
|
|
return {
|
|
"current": path,
|
|
"parent": os.path.dirname(path) if path != "/" else None,
|
|
"entries": entries
|
|
}
|
|
except PermissionError:
|
|
return {"error": "Zugriff verweigert", "entries": []}
|
|
|
|
|
|
# ============ BEREICH 1: Postfächer ============
|
|
|
|
@router.get("/postfaecher", response_model=List[PostfachResponse])
|
|
def liste_postfaecher(db: Session = Depends(get_db)):
|
|
return db.query(Postfach).all()
|
|
|
|
|
|
@router.post("/postfaecher", response_model=PostfachResponse)
|
|
def erstelle_postfach(data: PostfachCreate, db: Session = Depends(get_db)):
|
|
postfach = Postfach(**data.dict())
|
|
db.add(postfach)
|
|
db.commit()
|
|
db.refresh(postfach)
|
|
return postfach
|
|
|
|
|
|
@router.put("/postfaecher/{id}", response_model=PostfachResponse)
|
|
def aktualisiere_postfach(id: int, data: PostfachCreate, db: Session = Depends(get_db)):
|
|
postfach = db.query(Postfach).filter(Postfach.id == id).first()
|
|
if not postfach:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
|
|
update_data = data.dict()
|
|
# Passwort nur aktualisieren wenn nicht leer
|
|
if not update_data.get("passwort"):
|
|
del update_data["passwort"]
|
|
|
|
for key, value in update_data.items():
|
|
setattr(postfach, key, value)
|
|
|
|
db.commit()
|
|
db.refresh(postfach)
|
|
return postfach
|
|
|
|
|
|
@router.delete("/postfaecher/{id}")
|
|
def loesche_postfach(id: int, db: Session = Depends(get_db)):
|
|
postfach = db.query(Postfach).filter(Postfach.id == id).first()
|
|
if not postfach:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
db.delete(postfach)
|
|
db.commit()
|
|
return {"message": "Gelöscht"}
|
|
|
|
|
|
@router.post("/postfaecher/{id}/test")
|
|
def teste_postfach(id: int, db: Session = Depends(get_db)):
|
|
postfach = db.query(Postfach).filter(Postfach.id == id).first()
|
|
if not postfach:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
|
|
fetcher = MailFetcher({
|
|
"imap_server": postfach.imap_server,
|
|
"imap_port": postfach.imap_port,
|
|
"email": postfach.email,
|
|
"passwort": postfach.passwort,
|
|
"ordner": postfach.ordner
|
|
})
|
|
return fetcher.test_connection()
|
|
|
|
|
|
@router.get("/postfaecher/{id}/abrufen/stream")
|
|
def rufe_postfach_ab_stream(id: int, db: Session = Depends(get_db)):
|
|
"""Streaming-Endpoint für Mail-Abruf mit Live-Updates"""
|
|
postfach = db.query(Postfach).filter(Postfach.id == id).first()
|
|
if not postfach:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
|
|
# Daten kopieren für Generator (Session ist nach return nicht mehr verfügbar)
|
|
pf_data = {
|
|
"id": postfach.id,
|
|
"name": postfach.name,
|
|
"imap_server": postfach.imap_server,
|
|
"imap_port": postfach.imap_port,
|
|
"email": postfach.email,
|
|
"passwort": postfach.passwort,
|
|
"ordner": postfach.ordner,
|
|
"alle_ordner": postfach.alle_ordner,
|
|
"erlaubte_typen": postfach.erlaubte_typen,
|
|
"max_groesse_mb": postfach.max_groesse_mb,
|
|
"ziel_ordner": postfach.ziel_ordner
|
|
}
|
|
|
|
# Bereits verarbeitete Message-IDs laden
|
|
bereits_verarbeitet = set(
|
|
row.message_id for row in
|
|
db.query(VerarbeiteteMail.message_id)
|
|
.filter(VerarbeiteteMail.postfach_id == id)
|
|
.all()
|
|
)
|
|
|
|
def event_generator():
|
|
from ..models.database import SessionLocal
|
|
|
|
def send_event(data):
|
|
return f"data: {json.dumps(data)}\n\n"
|
|
|
|
yield send_event({"type": "start", "postfach": pf_data["name"], "bereits_verarbeitet": len(bereits_verarbeitet)})
|
|
|
|
# Zielordner erstellen
|
|
ziel = Path(pf_data["ziel_ordner"])
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
|
|
fetcher = MailFetcher({
|
|
"imap_server": pf_data["imap_server"],
|
|
"imap_port": pf_data["imap_port"],
|
|
"email": pf_data["email"],
|
|
"passwort": pf_data["passwort"],
|
|
"ordner": pf_data["ordner"],
|
|
"erlaubte_typen": pf_data["erlaubte_typen"],
|
|
"max_groesse_mb": pf_data["max_groesse_mb"]
|
|
})
|
|
|
|
attachments = []
|
|
|
|
try:
|
|
# Generator für streaming
|
|
for event in fetcher.fetch_attachments_generator(
|
|
ziel,
|
|
nur_ungelesen=False,
|
|
alle_ordner=pf_data["alle_ordner"],
|
|
bereits_verarbeitet=bereits_verarbeitet
|
|
):
|
|
yield send_event(event)
|
|
|
|
if event.get("type") == "datei":
|
|
attachments.append(event)
|
|
|
|
# DB-Session für Speicherung
|
|
session = SessionLocal()
|
|
try:
|
|
verarbeitete_msg_ids = set()
|
|
for att in attachments:
|
|
msg_id = att.get("message_id")
|
|
if msg_id and msg_id not in verarbeitete_msg_ids:
|
|
verarbeitete_msg_ids.add(msg_id)
|
|
session.add(VerarbeiteteMail(
|
|
postfach_id=pf_data["id"],
|
|
message_id=msg_id,
|
|
ordner=att.get("ordner", ""),
|
|
betreff=att.get("betreff", "")[:500] if att.get("betreff") else None,
|
|
absender=att.get("absender", "")[:255] if att.get("absender") else None,
|
|
anzahl_attachments=1
|
|
))
|
|
|
|
# Postfach aktualisieren
|
|
pf = session.query(Postfach).filter(Postfach.id == pf_data["id"]).first()
|
|
if pf:
|
|
pf.letzter_abruf = datetime.utcnow()
|
|
pf.letzte_anzahl = len(attachments)
|
|
session.commit()
|
|
finally:
|
|
session.close()
|
|
|
|
yield send_event({"type": "fertig", "anzahl": len(attachments)})
|
|
|
|
except Exception as e:
|
|
yield send_event({"type": "fehler", "nachricht": str(e)})
|
|
finally:
|
|
fetcher.disconnect()
|
|
|
|
return StreamingResponse(
|
|
event_generator(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no"
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/postfaecher/{id}/abrufen")
|
|
def rufe_postfach_ab(id: int, db: Session = Depends(get_db)):
|
|
postfach = db.query(Postfach).filter(Postfach.id == id).first()
|
|
if not postfach:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
|
|
# Bereits verarbeitete Message-IDs laden
|
|
bereits_verarbeitet = set(
|
|
row.message_id for row in
|
|
db.query(VerarbeiteteMail.message_id)
|
|
.filter(VerarbeiteteMail.postfach_id == id)
|
|
.all()
|
|
)
|
|
|
|
# Zielordner erstellen
|
|
ziel = Path(postfach.ziel_ordner)
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
|
|
fetcher = MailFetcher({
|
|
"imap_server": postfach.imap_server,
|
|
"imap_port": postfach.imap_port,
|
|
"email": postfach.email,
|
|
"passwort": postfach.passwort,
|
|
"ordner": postfach.ordner,
|
|
"erlaubte_typen": postfach.erlaubte_typen,
|
|
"max_groesse_mb": postfach.max_groesse_mb
|
|
})
|
|
|
|
try:
|
|
attachments = fetcher.fetch_attachments(
|
|
ziel,
|
|
nur_ungelesen=False, # Alle Mails durchsuchen
|
|
alle_ordner=postfach.alle_ordner,
|
|
bereits_verarbeitet=bereits_verarbeitet
|
|
)
|
|
|
|
# Verarbeitete Mails in DB speichern
|
|
verarbeitete_msg_ids = set()
|
|
for att in attachments:
|
|
msg_id = att.get("message_id")
|
|
if msg_id and msg_id not in verarbeitete_msg_ids:
|
|
verarbeitete_msg_ids.add(msg_id)
|
|
db.add(VerarbeiteteMail(
|
|
postfach_id=id,
|
|
message_id=msg_id,
|
|
ordner=att.get("ordner", ""),
|
|
betreff=att.get("betreff", "")[:500] if att.get("betreff") else None,
|
|
absender=att.get("absender", "")[:255] if att.get("absender") else None,
|
|
anzahl_attachments=1
|
|
))
|
|
|
|
postfach.letzter_abruf = datetime.utcnow()
|
|
postfach.letzte_anzahl = len(attachments)
|
|
db.commit()
|
|
|
|
return {
|
|
"ergebnisse": [{
|
|
"postfach": postfach.name,
|
|
"anzahl": len(attachments),
|
|
"dateien": [a["original_name"] for a in attachments],
|
|
"bereits_verarbeitet": len(bereits_verarbeitet)
|
|
}]
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"ergebnisse": [{
|
|
"postfach": postfach.name,
|
|
"fehler": str(e)
|
|
}]
|
|
}
|
|
finally:
|
|
fetcher.disconnect()
|
|
|
|
|
|
@router.post("/postfaecher/abrufen-alle")
|
|
def rufe_alle_postfaecher_ab(db: Session = Depends(get_db)):
|
|
postfaecher = db.query(Postfach).filter(Postfach.aktiv == True).all()
|
|
ergebnisse = []
|
|
|
|
for postfach in postfaecher:
|
|
ziel = Path(postfach.ziel_ordner)
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
|
|
fetcher = MailFetcher({
|
|
"imap_server": postfach.imap_server,
|
|
"imap_port": postfach.imap_port,
|
|
"email": postfach.email,
|
|
"passwort": postfach.passwort,
|
|
"ordner": postfach.ordner,
|
|
"erlaubte_typen": postfach.erlaubte_typen,
|
|
"max_groesse_mb": postfach.max_groesse_mb
|
|
})
|
|
|
|
try:
|
|
attachments = fetcher.fetch_attachments(ziel)
|
|
postfach.letzter_abruf = datetime.utcnow()
|
|
postfach.letzte_anzahl = len(attachments)
|
|
|
|
ergebnisse.append({
|
|
"postfach": postfach.name,
|
|
"anzahl": len(attachments),
|
|
"dateien": [a["original_name"] for a in attachments]
|
|
})
|
|
except Exception as e:
|
|
ergebnisse.append({
|
|
"postfach": postfach.name,
|
|
"fehler": str(e)
|
|
})
|
|
finally:
|
|
fetcher.disconnect()
|
|
|
|
db.commit()
|
|
return {"ergebnisse": ergebnisse}
|
|
|
|
|
|
# ============ BEREICH 2: Quell-Ordner ============
|
|
|
|
@router.get("/ordner", response_model=List[OrdnerResponse])
|
|
def liste_ordner(db: Session = Depends(get_db)):
|
|
return db.query(QuellOrdner).all()
|
|
|
|
|
|
@router.post("/ordner", response_model=OrdnerResponse)
|
|
def erstelle_ordner(data: OrdnerCreate, db: Session = Depends(get_db)):
|
|
ordner = QuellOrdner(**data.dict())
|
|
db.add(ordner)
|
|
db.commit()
|
|
db.refresh(ordner)
|
|
return ordner
|
|
|
|
|
|
@router.delete("/ordner/{id}")
|
|
def loesche_ordner(id: int, db: Session = Depends(get_db)):
|
|
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
|
|
if not ordner:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
db.delete(ordner)
|
|
db.commit()
|
|
return {"message": "Gelöscht"}
|
|
|
|
|
|
@router.get("/ordner/{id}/scannen")
|
|
def scanne_ordner(id: int, db: Session = Depends(get_db)):
|
|
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
|
|
if not ordner:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
|
|
pfad = Path(ordner.pfad)
|
|
if not pfad.exists():
|
|
return {"anzahl": 0, "fehler": "Ordner existiert nicht"}
|
|
|
|
# Dateien sammeln (rekursiv oder nicht)
|
|
dateien = []
|
|
pattern = "**/*" if ordner.rekursiv else "*"
|
|
for f in pfad.glob(pattern):
|
|
if f.is_file() and f.suffix.lower() in [t.lower() for t in ordner.dateitypen]:
|
|
dateien.append(f)
|
|
|
|
return {"anzahl": len(dateien), "dateien": [str(f.relative_to(pfad)) for f in dateien[:30]]}
|
|
|
|
|
|
# ============ Regeln ============
|
|
|
|
@router.get("/regeln", response_model=List[RegelResponse])
|
|
def liste_regeln(db: Session = Depends(get_db)):
|
|
return db.query(SortierRegel).order_by(SortierRegel.prioritaet).all()
|
|
|
|
|
|
@router.post("/regeln", response_model=RegelResponse)
|
|
def erstelle_regel(data: RegelCreate, db: Session = Depends(get_db)):
|
|
regel = SortierRegel(**data.dict())
|
|
db.add(regel)
|
|
db.commit()
|
|
db.refresh(regel)
|
|
return regel
|
|
|
|
|
|
@router.put("/regeln/{id}", response_model=RegelResponse)
|
|
def aktualisiere_regel(id: int, data: RegelCreate, db: Session = Depends(get_db)):
|
|
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
|
|
if not regel:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
for key, value in data.dict().items():
|
|
setattr(regel, key, value)
|
|
db.commit()
|
|
db.refresh(regel)
|
|
return regel
|
|
|
|
|
|
@router.delete("/regeln/{id}")
|
|
def loesche_regel(id: int, db: Session = Depends(get_db)):
|
|
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
|
|
if not regel:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
db.delete(regel)
|
|
db.commit()
|
|
return {"message": "Gelöscht"}
|
|
|
|
|
|
@router.post("/regeln/test")
|
|
def teste_regel(data: RegelTestRequest):
|
|
regel = data.regel
|
|
regel["aktiv"] = True
|
|
regel["prioritaet"] = 1
|
|
|
|
sorter = Sorter([regel])
|
|
doc_info = {"text": data.text, "original_name": "test.pdf", "absender": ""}
|
|
|
|
passend = sorter.finde_passende_regel(doc_info)
|
|
|
|
if passend:
|
|
extrahiert = sorter.extrahiere_felder(passend, doc_info)
|
|
dateiname = sorter.generiere_dateinamen(passend, extrahiert)
|
|
return {"passt": True, "extrahiert": extrahiert, "dateiname": dateiname}
|
|
|
|
return {"passt": False}
|
|
|
|
|
|
# ============ Sortierung ============
|
|
|
|
def sammle_dateien(ordner: QuellOrdner) -> list:
|
|
"""Sammelt alle Dateien aus einem Ordner (rekursiv oder nicht)"""
|
|
pfad = Path(ordner.pfad)
|
|
if not pfad.exists():
|
|
return []
|
|
|
|
dateien = []
|
|
pattern = "**/*" if ordner.rekursiv else "*"
|
|
erlaubte = [t.lower() for t in (ordner.dateitypen or [".pdf"])]
|
|
|
|
for f in pfad.glob(pattern):
|
|
if f.is_file() and f.suffix.lower() in erlaubte:
|
|
dateien.append(f)
|
|
|
|
return dateien
|
|
|
|
|
|
@router.post("/sortierung/starten")
|
|
def starte_sortierung(db: Session = Depends(get_db)):
|
|
ordner_liste = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all()
|
|
regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all()
|
|
|
|
if not ordner_liste:
|
|
return {"fehler": "Keine Quell-Ordner konfiguriert", "verarbeitet": []}
|
|
if not regeln:
|
|
return {"fehler": "Keine Regeln definiert", "verarbeitet": []}
|
|
|
|
# Regeln in Dict-Format
|
|
regeln_dicts = []
|
|
for r in regeln:
|
|
regeln_dicts.append({
|
|
"id": r.id,
|
|
"name": r.name,
|
|
"prioritaet": r.prioritaet,
|
|
"muster": r.muster,
|
|
"extraktion": r.extraktion,
|
|
"schema": r.schema,
|
|
"unterordner": r.unterordner
|
|
})
|
|
|
|
sorter = Sorter(regeln_dicts)
|
|
pdf_processor = PDFProcessor()
|
|
|
|
ergebnis = {
|
|
"gesamt": 0,
|
|
"sortiert": 0,
|
|
"zugferd": 0,
|
|
"fehler": 0,
|
|
"verarbeitet": []
|
|
}
|
|
|
|
for quell_ordner in ordner_liste:
|
|
pfad = Path(quell_ordner.pfad)
|
|
if not pfad.exists():
|
|
continue
|
|
|
|
ziel_basis = Path(quell_ordner.ziel_ordner)
|
|
dateien = sammle_dateien(quell_ordner)
|
|
|
|
for datei in dateien:
|
|
ergebnis["gesamt"] += 1
|
|
# Relativer Pfad für Anzeige
|
|
try:
|
|
rel_pfad = str(datei.relative_to(pfad))
|
|
except:
|
|
rel_pfad = datei.name
|
|
datei_info = {"original": rel_pfad}
|
|
|
|
try:
|
|
ist_pdf = datei.suffix.lower() == ".pdf"
|
|
text = ""
|
|
ist_zugferd = False
|
|
ocr_gemacht = False
|
|
|
|
# Nur PDFs durch den PDF-Processor
|
|
if ist_pdf:
|
|
pdf_result = pdf_processor.verarbeite(str(datei))
|
|
|
|
if pdf_result.get("fehler"):
|
|
raise Exception(pdf_result["fehler"])
|
|
|
|
text = pdf_result.get("text", "")
|
|
ist_zugferd = pdf_result.get("ist_zugferd", False)
|
|
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
|
|
|
|
# ZUGFeRD separat behandeln
|
|
if ist_zugferd:
|
|
zugferd_ziel = ziel_basis / "zugferd"
|
|
zugferd_ziel.mkdir(parents=True, exist_ok=True)
|
|
|
|
neuer_pfad = zugferd_ziel / datei.name
|
|
counter = 1
|
|
while neuer_pfad.exists():
|
|
neuer_pfad = zugferd_ziel / f"{datei.stem}_{counter}{datei.suffix}"
|
|
counter += 1
|
|
|
|
datei.rename(neuer_pfad)
|
|
|
|
ergebnis["zugferd"] += 1
|
|
datei_info["zugferd"] = True
|
|
datei_info["neuer_name"] = neuer_pfad.name
|
|
|
|
db.add(VerarbeiteteDatei(
|
|
original_pfad=str(datei),
|
|
original_name=datei.name,
|
|
neuer_pfad=str(neuer_pfad),
|
|
neuer_name=neuer_pfad.name,
|
|
ist_zugferd=True,
|
|
status="zugferd"
|
|
))
|
|
ergebnis["verarbeitet"].append(datei_info)
|
|
continue
|
|
|
|
# Regel finden (für PDFs mit Text, für andere nur Dateiname)
|
|
doc_info = {
|
|
"text": text,
|
|
"original_name": datei.name,
|
|
"absender": "",
|
|
"dateityp": datei.suffix.lower()
|
|
}
|
|
|
|
regel = sorter.finde_passende_regel(doc_info)
|
|
|
|
if not regel:
|
|
datei_info["fehler"] = "Keine passende Regel"
|
|
ergebnis["fehler"] += 1
|
|
ergebnis["verarbeitet"].append(datei_info)
|
|
continue
|
|
|
|
# Felder extrahieren
|
|
extrahiert = sorter.extrahiere_felder(regel, doc_info)
|
|
|
|
# Dateiendung beibehalten
|
|
schema = regel.get("schema", "{datum} - Dokument.pdf")
|
|
# Endung aus Schema entfernen und Original-Endung anhängen
|
|
if schema.endswith(".pdf"):
|
|
schema = schema[:-4] + datei.suffix
|
|
neuer_name = sorter.generiere_dateinamen({"schema": schema, **regel}, extrahiert)
|
|
|
|
# Zielordner
|
|
ziel = ziel_basis
|
|
if regel.get("unterordner"):
|
|
ziel = ziel / regel["unterordner"]
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Verschieben
|
|
neuer_pfad = sorter.verschiebe_datei(str(datei), str(ziel), neuer_name)
|
|
|
|
ergebnis["sortiert"] += 1
|
|
datei_info["neuer_name"] = neuer_name
|
|
|
|
db.add(VerarbeiteteDatei(
|
|
original_pfad=str(datei),
|
|
original_name=datei.name,
|
|
neuer_pfad=neuer_pfad,
|
|
neuer_name=neuer_name,
|
|
ist_zugferd=False,
|
|
ocr_durchgefuehrt=ocr_gemacht,
|
|
status="sortiert",
|
|
extrahierte_daten=extrahiert
|
|
))
|
|
|
|
except Exception as e:
|
|
ergebnis["fehler"] += 1
|
|
datei_info["fehler"] = str(e)
|
|
|
|
ergebnis["verarbeitet"].append(datei_info)
|
|
|
|
db.commit()
|
|
return ergebnis
|
|
|
|
|
|
@router.get("/health")
|
|
def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
# ============ Einfache Regeln (UI-freundlich) ============
|
|
|
|
@router.get("/dokumenttypen")
|
|
def liste_dokumenttypen():
|
|
"""Gibt alle verfügbaren Dokumenttypen für das UI zurück"""
|
|
from ..modules.sorter import DOKUMENTTYPEN
|
|
return [
|
|
{"id": key, "name": config["name"], "schema": config["schema"], "unterordner": config["unterordner"]}
|
|
for key, config in DOKUMENTTYPEN.items()
|
|
]
|
|
|
|
|
|
class EinfacheRegelCreate(BaseModel):
|
|
name: str
|
|
dokumenttyp: str # z.B. "rechnung", "vertrag"
|
|
keywords: str # Komma-getrennt
|
|
firma: Optional[str] = None # Fester Firmenwert
|
|
unterordner: Optional[str] = None
|
|
prioritaet: int = 50
|
|
|
|
|
|
@router.post("/regeln/einfach")
|
|
def erstelle_einfache_regel_api(data: EinfacheRegelCreate, db: Session = Depends(get_db)):
|
|
"""Erstellt eine Regel basierend auf Dokumenttyp - für einfaches UI"""
|
|
from ..modules.sorter import DOKUMENTTYPEN
|
|
|
|
typ_config = DOKUMENTTYPEN.get(data.dokumenttyp, DOKUMENTTYPEN["sonstiges"])
|
|
|
|
# Muster als Dict (keywords werden vom Sorter geparst)
|
|
muster = {"keywords": data.keywords}
|
|
|
|
# Extraktion (nur Firma wenn angegeben)
|
|
extraktion = {}
|
|
if data.firma:
|
|
extraktion["firma"] = {"wert": data.firma}
|
|
|
|
regel = SortierRegel(
|
|
name=data.name,
|
|
prioritaet=data.prioritaet,
|
|
aktiv=True,
|
|
muster=muster,
|
|
extraktion=extraktion,
|
|
schema=typ_config["schema"],
|
|
unterordner=data.unterordner or typ_config["unterordner"]
|
|
)
|
|
|
|
db.add(regel)
|
|
db.commit()
|
|
db.refresh(regel)
|
|
|
|
return {
|
|
"id": regel.id,
|
|
"name": regel.name,
|
|
"dokumenttyp": data.dokumenttyp,
|
|
"keywords": data.keywords,
|
|
"schema": regel.schema
|
|
}
|
|
|
|
|
|
class ExtraktionTestRequest(BaseModel):
|
|
text: str
|
|
dateiname: Optional[str] = "test.pdf"
|
|
|
|
|
|
@router.post("/extraktion/test")
|
|
def teste_extraktion(data: ExtraktionTestRequest):
|
|
"""Testet die automatische Extraktion auf einem Text"""
|
|
from ..modules.extraktoren import extrahiere_alle_felder, baue_dateiname
|
|
|
|
dokument_info = {
|
|
"original_name": data.dateiname,
|
|
"absender": ""
|
|
}
|
|
|
|
# Felder extrahieren
|
|
felder = extrahiere_alle_felder(data.text, dokument_info)
|
|
|
|
# Beispiel-Dateinamen für verschiedene Typen generieren
|
|
beispiele = {}
|
|
from ..modules.sorter import DOKUMENTTYPEN
|
|
for typ_id, typ_config in DOKUMENTTYPEN.items():
|
|
beispiele[typ_id] = baue_dateiname(typ_config["schema"], felder, ".pdf")
|
|
|
|
return {
|
|
"extrahiert": felder,
|
|
"beispiel_dateinamen": beispiele
|
|
}
|
|
|
|
|
|
@router.post("/regeln/{id}/vorschau")
|
|
def regel_vorschau(id: int, data: ExtraktionTestRequest, db: Session = Depends(get_db)):
|
|
"""Zeigt Vorschau wie eine Regel auf einen Text angewendet würde"""
|
|
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
|
|
if not regel:
|
|
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
|
|
|
|
from ..modules.sorter import Sorter
|
|
|
|
sorter = Sorter([{
|
|
"id": regel.id,
|
|
"name": regel.name,
|
|
"prioritaet": regel.prioritaet,
|
|
"aktiv": True,
|
|
"muster": regel.muster,
|
|
"extraktion": regel.extraktion,
|
|
"schema": regel.schema,
|
|
"unterordner": regel.unterordner
|
|
}])
|
|
|
|
dokument_info = {
|
|
"text": data.text,
|
|
"original_name": data.dateiname or "test.pdf",
|
|
"absender": ""
|
|
}
|
|
|
|
# Prüfen ob Regel matched
|
|
passende_regel = sorter.finde_passende_regel(dokument_info)
|
|
|
|
if not passende_regel:
|
|
return {
|
|
"matched": False,
|
|
"grund": "Keywords nicht gefunden"
|
|
}
|
|
|
|
# Felder extrahieren
|
|
felder = sorter.extrahiere_felder(passende_regel, dokument_info)
|
|
|
|
# Dateiname generieren
|
|
dateiname = sorter.generiere_dateinamen(passende_regel, felder)
|
|
|
|
return {
|
|
"matched": True,
|
|
"extrahiert": felder,
|
|
"dateiname": dateiname,
|
|
"unterordner": passende_regel.get("unterordner")
|
|
}
|