276 lines
9.8 KiB
Python
Executable file
276 lines
9.8 KiB
Python
Executable file
"""Datenbank-Modelle - Getrennte Bereiche: Mail-Abruf und Datei-Sortierung"""
|
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Text, JSON, ForeignKey
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import sessionmaker
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import event
|
|
from ..config import DATABASE_URL
|
|
|
|
# Datenbank-Engine erstellen (SQLite oder MariaDB)
|
|
is_sqlite = DATABASE_URL.startswith("sqlite")
|
|
|
|
if is_sqlite:
|
|
# SQLite mit WAL-Modus und Timeout für bessere Concurrency
|
|
engine = create_engine(
|
|
DATABASE_URL,
|
|
echo=False,
|
|
connect_args={"check_same_thread": False, "timeout": 30}
|
|
)
|
|
|
|
# WAL-Modus aktivieren für bessere gleichzeitige Zugriffe
|
|
@event.listens_for(engine, "connect")
|
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
|
cursor = dbapi_connection.cursor()
|
|
cursor.execute("PRAGMA journal_mode=WAL")
|
|
cursor.execute("PRAGMA busy_timeout=30000")
|
|
cursor.close()
|
|
else:
|
|
# MariaDB/MySQL
|
|
engine = create_engine(
|
|
DATABASE_URL,
|
|
echo=False,
|
|
pool_pre_ping=True,
|
|
pool_recycle=3600
|
|
)
|
|
|
|
SessionLocal = sessionmaker(bind=engine)
|
|
Base = declarative_base()
|
|
|
|
|
|
# ============ BEREICH 1: Mail-Abruf ============
|
|
|
|
class Postfach(Base):
|
|
"""IMAP-Postfach Konfiguration"""
|
|
__tablename__ = "postfaecher"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(100), nullable=False)
|
|
|
|
# IMAP
|
|
imap_server = Column(String(255), nullable=False)
|
|
imap_port = Column(Integer, default=993)
|
|
email = Column(String(255), nullable=False)
|
|
passwort = Column(String(255), nullable=False)
|
|
ordner = Column(String(100), default="INBOX")
|
|
alle_ordner = Column(Boolean, default=False) # Alle IMAP-Ordner durchsuchen
|
|
nur_ungelesen = Column(Boolean, default=False) # Nur ungelesene Mails (False = alle)
|
|
|
|
# Ziel
|
|
ziel_ordner = Column(String(500), nullable=False)
|
|
|
|
# Filter
|
|
erlaubte_typen = Column(JSON, default=lambda: [".pdf"])
|
|
max_groesse_mb = Column(Integer, default=25)
|
|
min_groesse_kb = Column(Integer, default=10) # Mindestgröße in KB (gegen Icons)
|
|
ab_datum = Column(DateTime) # Nur Mails ab diesem Datum verarbeiten
|
|
# Größenfilter pro Dateityp: {".pdf": {"min_kb": 10, "max_mb": 25}, ".jpg": {"min_kb": 50, "max_mb": 10}}
|
|
groessen_filter = Column(JSON, default=lambda: {})
|
|
|
|
# Status
|
|
aktiv = Column(Boolean, default=True)
|
|
letzter_abruf = Column(DateTime)
|
|
letzte_anzahl = Column(Integer, default=0)
|
|
|
|
|
|
# ============ BEREICH 2: Datei-Sortierung ============
|
|
|
|
class QuellOrdner(Base):
|
|
"""Ordner der nach Dateien gescannt wird"""
|
|
__tablename__ = "quell_ordner"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(100), nullable=False)
|
|
pfad = Column(String(500), nullable=False)
|
|
ziel_ordner = Column(String(500), nullable=False)
|
|
rekursiv = Column(Boolean, default=True) # Unterordner einschließen
|
|
dateitypen = Column(JSON, default=lambda: [".pdf", ".jpg", ".jpeg", ".png", ".tiff"])
|
|
# ZUGFeRD-Behandlung: "separieren", "regel", "normal", "ignorieren"
|
|
zugferd_behandlung = Column(String(20), default="separieren")
|
|
# Signierte PDFs: "normal", "separieren", "regel", "ignorieren"
|
|
signiert_behandlung = Column(String(20), default="normal")
|
|
aktiv = Column(Boolean, default=True)
|
|
# NEU: Direkt verschieben ohne Regelprüfung
|
|
direkt_verschieben = Column(Boolean, default=False)
|
|
# OCR-Optionen
|
|
ocr_aktivieren = Column(Boolean, default=True) # OCR für gescannte PDFs
|
|
original_sichern = Column(String(500)) # Ordner für Original-Backup (vor OCR)
|
|
|
|
# Status (wie bei Postfächern)
|
|
letzte_verarbeitung = Column(DateTime)
|
|
letzte_anzahl = Column(Integer, default=0)
|
|
|
|
|
|
class SortierRegel(Base):
|
|
"""Regeln für Datei-Erkennung und Benennung"""
|
|
__tablename__ = "sortier_regeln"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(100), nullable=False)
|
|
prioritaet = Column(Integer, default=100)
|
|
aktiv = Column(Boolean, default=True)
|
|
|
|
# Erkennungsmuster (JSON mit keywords, text_match, text_regex, etc.)
|
|
# NEU: Auch negative Muster möglich (keywords_nicht, text_not_match)
|
|
muster = Column(JSON, default=dict)
|
|
|
|
# Extraktion (JSON mit datum, betrag, nummer, firma, etc.)
|
|
extraktion = Column(JSON, default=dict)
|
|
|
|
# Ausgabe
|
|
schema = Column(String(500), default="{datum} - Dokument.pdf")
|
|
unterordner = Column(String(100)) # Optional: Unterordner im Ziel
|
|
|
|
# NEU: Fallback-Regel (greift wenn keine andere Regel passt)
|
|
ist_fallback = Column(Boolean, default=False)
|
|
|
|
# Freie Ordner (zusätzlich zu den zugewiesenen Quell-Ordnern)
|
|
freie_ordner = Column(JSON, default=list)
|
|
|
|
# Ziel-Ordner für diese Regel (optional, überschreibt Quell-Ordner Ziel)
|
|
ziel_ordner = Column(String(500))
|
|
|
|
# Nur umbenennen, nicht verschieben (Dateien bleiben im Quellordner)
|
|
nur_umbenennen = Column(Boolean, default=False)
|
|
|
|
|
|
class OrdnerRegel(Base):
|
|
"""Verknüpfung zwischen Quell-Ordnern und Sortier-Regeln"""
|
|
__tablename__ = "ordner_regeln"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
ordner_id = Column(Integer, ForeignKey("quell_ordner.id", ondelete="CASCADE"), nullable=False)
|
|
regel_id = Column(Integer, ForeignKey("sortier_regeln.id", ondelete="CASCADE"), nullable=False)
|
|
|
|
|
|
class Zeitplan(Base):
|
|
"""Scheduler-Konfiguration für automatische Ausführung"""
|
|
__tablename__ = "zeitplaene"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(100), nullable=False)
|
|
aktiv = Column(Boolean, default=True)
|
|
|
|
# Was wird ausgeführt
|
|
typ = Column(String(50), nullable=False) # "mail_abruf", "grobsortierung", "sortierregeln"
|
|
postfach_id = Column(Integer) # Optional: spezifisches Postfach
|
|
quell_ordner_id = Column(Integer) # Optional: spezifischer Quellordner
|
|
regel_id = Column(Integer) # Optional: spezifische Regel (für sortierregeln)
|
|
|
|
# Zeitplan-Intervall
|
|
intervall = Column(String(20), nullable=False) # "stündlich", "täglich", "wöchentlich", "monatlich"
|
|
stunde = Column(Integer, default=6) # Uhrzeit (0-23)
|
|
minute = Column(Integer, default=0) # Minute (0-59)
|
|
wochentag = Column(Integer) # 0=Montag, 6=Sonntag (für wöchentlich)
|
|
monatstag = Column(Integer) # 1-28 (für monatlich)
|
|
|
|
# Status
|
|
letzte_ausfuehrung = Column(DateTime)
|
|
naechste_ausfuehrung = Column(DateTime)
|
|
letzter_status = Column(String(50)) # "erfolg", "fehler"
|
|
letzte_meldung = Column(Text)
|
|
|
|
erstellt_am = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
class VerarbeiteteMail(Base):
|
|
"""Tracking welche Mails bereits verarbeitet wurden"""
|
|
__tablename__ = "verarbeitete_mails"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
postfach_id = Column(Integer, nullable=False)
|
|
message_id = Column(String(500), nullable=False) # Email Message-ID Header
|
|
ordner = Column(String(200)) # IMAP Ordner
|
|
betreff = Column(String(500))
|
|
absender = Column(String(255))
|
|
anzahl_attachments = Column(Integer, default=0)
|
|
verarbeitet_am = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
class VerarbeiteteDatei(Base):
|
|
"""Log verarbeiteter Dateien"""
|
|
__tablename__ = "verarbeitete_dateien"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
original_pfad = Column(String(1000))
|
|
original_name = Column(String(500))
|
|
neuer_pfad = Column(String(1000))
|
|
neuer_name = Column(String(500))
|
|
|
|
ist_zugferd = Column(Boolean, default=False)
|
|
ocr_durchgefuehrt = Column(Boolean, default=False)
|
|
|
|
status = Column(String(50)) # sortiert, zugferd, fehler, keine_regel
|
|
fehler = Column(Text)
|
|
|
|
extrahierte_daten = Column(JSON)
|
|
verarbeitet_am = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
def migrate_db():
|
|
"""Fügt fehlende Spalten hinzu ohne Daten zu löschen"""
|
|
from sqlalchemy import inspect, text
|
|
|
|
inspector = inspect(engine)
|
|
|
|
# Migrations-Definitionen: {tabelle: {spalte: sql_typ}}
|
|
migrations = {
|
|
"postfaecher": {
|
|
"alle_ordner": "BOOLEAN DEFAULT 0",
|
|
"nur_ungelesen": "BOOLEAN DEFAULT 0",
|
|
"min_groesse_kb": "INTEGER DEFAULT 10",
|
|
"ab_datum": "DATETIME",
|
|
"groessen_filter": "JSON"
|
|
},
|
|
"quell_ordner": {
|
|
"rekursiv": "BOOLEAN DEFAULT 1",
|
|
"dateitypen": "JSON",
|
|
"zugferd_behandlung": "VARCHAR(20) DEFAULT 'separieren'",
|
|
"signiert_behandlung": "VARCHAR(20) DEFAULT 'normal'",
|
|
"direkt_verschieben": "BOOLEAN DEFAULT 0",
|
|
"ocr_aktivieren": "BOOLEAN DEFAULT 1",
|
|
"original_sichern": "VARCHAR(500)",
|
|
"letzte_verarbeitung": "DATETIME",
|
|
"letzte_anzahl": "INTEGER DEFAULT 0"
|
|
},
|
|
"sortier_regeln": {
|
|
"ist_fallback": "BOOLEAN DEFAULT 0",
|
|
"freie_ordner": "JSON",
|
|
"ziel_ordner": "VARCHAR(500)",
|
|
"nur_umbenennen": "BOOLEAN DEFAULT 0"
|
|
},
|
|
"zeitplaene": {
|
|
"regel_id": "INTEGER"
|
|
}
|
|
}
|
|
|
|
with engine.connect() as conn:
|
|
for table, columns in migrations.items():
|
|
if table not in inspector.get_table_names():
|
|
continue
|
|
|
|
existing = [col["name"] for col in inspector.get_columns(table)]
|
|
|
|
for col_name, col_type in columns.items():
|
|
if col_name not in existing:
|
|
try:
|
|
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}"))
|
|
conn.commit()
|
|
print(f"Migration: {table}.{col_name} hinzugefügt")
|
|
except Exception as e:
|
|
print(f"Migration übersprungen: {table}.{col_name} - {e}")
|
|
|
|
|
|
def init_db():
|
|
"""Datenbank initialisieren"""
|
|
Base.metadata.create_all(engine)
|
|
migrate_db()
|
|
|
|
|
|
def get_db():
|
|
"""Database Session Generator"""
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|