Feature: Rechnungs-System (invoices) — Backend komplett
- DB-Migration: invoices + invoice_items Tabellen inkl. Indizes - routes/invoices.py: vollständiger Admin-Router (prefix /api/admin/invoices) - CRUD: Liste, Detail, Erstellen, Senden, Bezahlen, Stornieren - PDF-Generierung via fpdf2 mit §14-UStG-Pflichtangaben (Kleinunternehmer-Hinweis) - Cashflow-Übersicht und Quartalsbericht - PDF-Download-Endpunkt - Speicherung in /scaninput + optionaler Paperless-Upload - mailer.py: send_email() + Backends um optionale PDF-Anhänge erweitert (Brevo: base64, SMTP: MIMEApplication) - main.py: invoices_router registriert - docker-compose.yml: /volume1/scaninput:/scaninput Volume hinzugefügt
This commit is contained in:
parent
9c359bb07e
commit
b68a12587a
5 changed files with 570 additions and 10 deletions
|
|
@ -6,11 +6,13 @@ Unterstützt zwei Backends (wird automatisch gewählt):
|
|||
"""
|
||||
|
||||
import os
|
||||
import base64
|
||||
import smtplib
|
||||
import asyncio
|
||||
import logging
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.application import MIMEApplication
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -33,9 +35,7 @@ APP_URL = os.getenv("APP_URL", "https://banyaro.app")
|
|||
# ------------------------------------------------------------------
|
||||
# Brevo REST-API
|
||||
# ------------------------------------------------------------------
|
||||
async def _send_brevo(to: str, subject: str, html: str, plain: str):
|
||||
# Absender-Name und -Adresse aus SMTP_FROM parsen
|
||||
# Format: "Ban Yaro <noreply@banyaro.app>" oder "noreply@banyaro.app"
|
||||
async def _send_brevo(to: str, subject: str, html: str, plain: str, attachments: list | None = None):
|
||||
from_raw = SMTP_FROM
|
||||
if "<" in from_raw:
|
||||
from_name = from_raw[:from_raw.index("<")].strip()
|
||||
|
|
@ -52,6 +52,14 @@ async def _send_brevo(to: str, subject: str, html: str, plain: str):
|
|||
"textContent": plain,
|
||||
"headers": {"X-Mailin-Track-Click": "0", "X-Mailin-Track-Opens": "0"},
|
||||
}
|
||||
if attachments:
|
||||
payload["attachment"] = [
|
||||
{
|
||||
"name": a["filename"],
|
||||
"content": base64.b64encode(a["content"]).decode("ascii"),
|
||||
}
|
||||
for a in attachments
|
||||
]
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
resp = await client.post(
|
||||
BREVO_API_URL,
|
||||
|
|
@ -64,13 +72,25 @@ async def _send_brevo(to: str, subject: str, html: str, plain: str):
|
|||
# ------------------------------------------------------------------
|
||||
# SMTP Fallback
|
||||
# ------------------------------------------------------------------
|
||||
def _send_smtp_sync(to: str, subject: str, html: str, plain: str):
|
||||
msg = MIMEMultipart("alternative")
|
||||
def _send_smtp_sync(to: str, subject: str, html: str, plain: str, attachments: list | None = None):
|
||||
if attachments:
|
||||
msg = MIMEMultipart("mixed")
|
||||
alt = MIMEMultipart("alternative")
|
||||
alt.attach(MIMEText(plain, "plain", "utf-8"))
|
||||
alt.attach(MIMEText(html, "html", "utf-8"))
|
||||
msg.attach(alt)
|
||||
for a in attachments:
|
||||
part = MIMEApplication(a["content"], Name=a["filename"])
|
||||
part["Content-Disposition"] = f'attachment; filename="{a["filename"]}"'
|
||||
msg.attach(part)
|
||||
else:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg.attach(MIMEText(plain, "plain", "utf-8"))
|
||||
msg.attach(MIMEText(html, "html", "utf-8"))
|
||||
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = SMTP_FROM
|
||||
msg["To"] = to
|
||||
msg.attach(MIMEText(plain, "plain", "utf-8"))
|
||||
msg.attach(MIMEText(html, "html", "utf-8"))
|
||||
|
||||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as s:
|
||||
s.ehlo()
|
||||
|
|
@ -83,10 +103,10 @@ def _send_smtp_sync(to: str, subject: str, html: str, plain: str):
|
|||
# ------------------------------------------------------------------
|
||||
# Öffentliche Funktion
|
||||
# ------------------------------------------------------------------
|
||||
async def send_email(to: str, subject: str, html: str, plain: str = ""):
|
||||
async def send_email(to: str, subject: str, html: str, plain: str = "", attachments: list | None = None):
|
||||
if BREVO_API_KEY:
|
||||
try:
|
||||
await _send_brevo(to, subject, html, plain)
|
||||
await _send_brevo(to, subject, html, plain, attachments)
|
||||
logger.info(f"Mail via Brevo gesendet: «{subject}» → {to}")
|
||||
return
|
||||
except Exception as e:
|
||||
|
|
@ -96,7 +116,9 @@ async def send_email(to: str, subject: str, html: str, plain: str = ""):
|
|||
if SMTP_HOST:
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
await loop.run_in_executor(None, _send_smtp_sync, to, subject, html, plain)
|
||||
await loop.run_in_executor(
|
||||
None, _send_smtp_sync, to, subject, html, plain, attachments
|
||||
)
|
||||
logger.info(f"Mail via SMTP gesendet: «{subject}» → {to}")
|
||||
return
|
||||
except Exception as e:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue