added amazon importer and logging smtp

This commit is contained in:
2026-03-20 16:22:38 +01:00
parent 9fdada5dbe
commit a4e39332c7
16 changed files with 2619 additions and 255 deletions
+155 -11
View File
@@ -4,13 +4,13 @@ import aiosqlite
from cryptography.fernet import Fernet
DB_PATH = os.environ.get("DB_PATH", "/data/belegimport.db")
SCHEMA_VERSION = 2
SCHEMA_VERSION = 8
logger = logging.getLogger(__name__)
_fernet = None
ENCRYPTED_KEYS = {"imap_password", "smtp_password", "smb_password"}
ENCRYPTED_KEYS = {"imap_password", "smtp_password", "smb_password", "amazon_password"}
DEFAULT_SETTINGS = {
"imap_server": "",
@@ -24,8 +24,12 @@ DEFAULT_SETTINGS = {
"smtp_username": "",
"smtp_password": "",
"import_email": "",
"import_email_eingang": "",
"import_email_ausgang": "",
"source_folder": "Rechnungen",
"processed_folder": "Rechnungen/Verarbeitet",
"source_folder_ausgang": "",
"processed_folder_ausgang": "",
"interval_minutes": "5",
"scheduler_enabled": "false",
"fetch_since_date": "",
@@ -39,7 +43,18 @@ DEFAULT_SETTINGS = {
"smb_share": "",
"smb_source_path": "",
"smb_processed_path": "Verarbeitet",
"smb_source_path_ausgang": "",
"smb_processed_path_ausgang": "",
"smb_mode": "forward",
# Amazon
"amazon_enabled": "false",
"amazon_email": "",
"amazon_password": "",
"amazon_domain": "amazon.de",
"amazon_last_sync": "",
"amazon_since_date": "",
# Debug
"debug_save_amazon_pdfs": "false",
}
@@ -121,7 +136,10 @@ async def _run_migrations(db: aiosqlite.Connection, current_version: int):
email_from TEXT,
attachments_count INTEGER DEFAULT 0,
status TEXT NOT NULL,
error_message TEXT
error_message TEXT,
sent_to TEXT DEFAULT '',
smtp_log TEXT DEFAULT '',
beleg_type TEXT DEFAULT 'eingang'
)
""")
await db.commit()
@@ -150,10 +168,72 @@ async def _run_migrations(db: aiosqlite.Connection, current_version: int):
await db.commit()
await _set_schema_version(db, 2)
# --- Future migrations go here ---
# if current_version < 3:
# logger.info("Migration v3: ...")
# await _set_schema_version(db, 3)
if current_version < 3:
logger.info("Migration v3: Amazon-Plattform hinzugefügt")
await db.execute("""
CREATE TABLE IF NOT EXISTS amazon_downloaded (
order_id TEXT PRIMARY KEY,
downloaded_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
)
""")
await db.commit()
await _set_schema_version(db, 3)
if current_version < 4:
logger.info("Migration v4: sent_to Spalte im Verarbeitungslog")
await db.execute("""
ALTER TABLE processing_log ADD COLUMN sent_to TEXT DEFAULT ''
""")
await db.commit()
await _set_schema_version(db, 4)
if current_version < 5:
logger.info("Migration v5: SMTP-Protokoll im Verarbeitungslog")
await db.execute("""
ALTER TABLE processing_log ADD COLUMN smtp_log TEXT DEFAULT ''
""")
await db.commit()
await _set_schema_version(db, 5)
if current_version < 6:
logger.info("Migration v6: Per-Invoice Tracking statt per-Order")
try:
await db.execute("""
ALTER TABLE amazon_downloaded ADD COLUMN invoice_url TEXT DEFAULT ''
""")
except Exception:
pass # column already exists
await db.commit()
await _set_schema_version(db, 6)
if current_version < 8:
logger.info("Migration v7/8: Eingangs-/Ausgangsbelege Unterscheidung")
# Add beleg_type column to processing_log (check if it exists first)
cursor = await db.execute("PRAGMA table_info(processing_log)")
columns = [row[1] for row in await cursor.fetchall()]
if "beleg_type" not in columns:
await db.execute("""
ALTER TABLE processing_log ADD COLUMN beleg_type TEXT DEFAULT 'eingang'
""")
logger.info(" beleg_type Spalte hinzugefügt")
# Migrate import_email -> import_email_eingang
cursor = await db.execute(
"SELECT value FROM settings WHERE key = 'import_email'"
)
row = await cursor.fetchone()
if row and row[0]:
cursor2 = await db.execute(
"SELECT value FROM settings WHERE key = 'import_email_eingang'"
)
row2 = await cursor2.fetchone()
if not row2 or not row2[0]:
await db.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('import_email_eingang', ?)",
(row[0],),
)
logger.info(" import_email nach import_email_eingang übertragen")
await db.commit()
await _set_schema_version(db, 8)
await db.commit()
@@ -176,7 +256,18 @@ async def init_db():
email_from TEXT,
attachments_count INTEGER DEFAULT 0,
status TEXT NOT NULL,
error_message TEXT
error_message TEXT,
sent_to TEXT DEFAULT '',
smtp_log TEXT DEFAULT '',
beleg_type TEXT DEFAULT 'eingang'
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS amazon_downloaded (
order_id TEXT NOT NULL,
downloaded_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
invoice_url TEXT DEFAULT '',
PRIMARY KEY (order_id, invoice_url)
)
""")
await db.commit()
@@ -235,19 +326,29 @@ async def save_settings(data: dict):
await db.commit()
def get_import_email(settings: dict, beleg_type: str = "eingang") -> str:
"""Resolve the correct import email address based on document type."""
if beleg_type == "ausgang":
return settings.get("import_email_ausgang", "")
return settings.get("import_email_eingang", "") or settings.get("import_email", "")
async def add_log_entry(
email_subject: str,
email_from: str,
attachments_count: int,
status: str,
error_message: str = "",
sent_to: str = "",
smtp_log: str = "",
beleg_type: str = "eingang",
):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"""INSERT INTO processing_log
(email_subject, email_from, attachments_count, status, error_message)
VALUES (?, ?, ?, ?, ?)""",
(email_subject, email_from, attachments_count, status, error_message),
(email_subject, email_from, attachments_count, status, error_message, sent_to, smtp_log, beleg_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(email_subject, email_from, attachments_count, status, error_message, sent_to, smtp_log, beleg_type),
)
await db.commit()
@@ -260,3 +361,46 @@ async def get_log_entries(limit: int = 100) -> list[dict]:
)
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def clear_log_entries() -> int:
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute("SELECT COUNT(*) FROM processing_log")
count = (await cursor.fetchone())[0]
await db.execute("DELETE FROM processing_log")
await db.commit()
return count
async def is_invoice_downloaded(order_id: str, invoice_url: str = "") -> bool:
"""Check if a specific invoice has been downloaded.
If invoice_url is given, check per-URL. Otherwise check per order_id."""
async with aiosqlite.connect(DB_PATH) as db:
if invoice_url:
cursor = await db.execute(
"SELECT 1 FROM amazon_downloaded WHERE order_id = ? AND invoice_url = ?",
(order_id, invoice_url),
)
else:
cursor = await db.execute(
"SELECT 1 FROM amazon_downloaded WHERE order_id = ?", (order_id,)
)
return await cursor.fetchone() is not None
async def mark_invoice_downloaded(order_id: str, invoice_url: str = ""):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT OR IGNORE INTO amazon_downloaded (order_id, invoice_url) VALUES (?, ?)",
(order_id, invoice_url),
)
await db.commit()
async def reset_downloaded_invoices() -> int:
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute("SELECT COUNT(*) FROM amazon_downloaded")
count = (await cursor.fetchone())[0]
await db.execute("DELETE FROM amazon_downloaded")
await db.commit()
return count