changes to he pdf sending

This commit is contained in:
2026-08-27 13:24:51 +02:00
parent 708274bd8f
commit cb6627bd3d
2 changed files with 81 additions and 70 deletions
+47 -40
View File
@@ -2206,7 +2206,19 @@ def build_module_config_string(package_list: list) -> str:
]
return " ".join(config_parts)
def background_booking_provisioning(app_instance, prov_id, subdomain, package_list, user_id, school_name, user_email, username, address, price, software_name):
def background_booking_provisioning(
app_instance,
prov_id,
subdomain,
package_list,
user_id,
school_name,
user_email,
username,
address,
price,
software_name="Invario Inventarsystem"
):
"""Hintergrund-Worker, der die Tenant-Bereitstellung über das Host-Watcher-Triggersystem auslöst."""
with app_instance.app_context():
req_client, req_col = _get_collection("instance_requests")
@@ -2214,7 +2226,7 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li
# 1. Status auf "in_progress" setzen
req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "in_progress"}})
# 2. Naechsten Port und Admin-Passwort ermitteln
# 2. Nächsten Port und Admin-Passwort ermitteln
inst_client_check, inst_col_check = _get_collection("instances")
try:
existing_instances = list(inst_col_check.find({}, {"https_port": 1, "port": 1}))
@@ -2232,7 +2244,7 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li
# Generiere den korrekten Modul-String
module_config_str = build_module_config_string(package_list)
# 3. Trigger-Datei fuer den Host-Watcher schreiben
# 3. Trigger-Datei für den Host-Watcher schreiben
os.makedirs(TRIGGER_DIR, exist_ok=True)
trigger_file = os.path.join(TRIGGER_DIR, f"{subdomain}.json")
@@ -2248,13 +2260,13 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li
with open(trigger_file, "w") as f:
json.dump(trigger_payload, f)
print(f"[INFO] Trigger-Datei fuer Host-Watcher erstellt: {trigger_file} mit Payload: {trigger_payload}")
print(f"[INFO] Trigger-Datei für Host-Watcher erstellt: {trigger_file} mit Payload: {trigger_payload}")
# 4. Instanz-Eintrag in der Datenbank registrieren
domain = f"{subdomain}.invario-software.de"
instance_doc = {
"owner_id": user_id,
"owner_username": username, # <--- HIER HINZUGEFÜGT
"owner_username": username,
"school_name": school_name,
"subdomain": subdomain,
"domain": domain,
@@ -2272,9 +2284,11 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li
finally:
inst_client.close()
# 5. Bestaetigungs-E-Mail versenden
# 5. Bestätigungs-E-Mail inkl. Vertrag & Anhängen versenden
try:
if user_email:
formatted_date = datetime.today().strftime('%d.%m.%Y')
send_accreditation_email(
recipient=user_email,
domain=domain,
@@ -2283,9 +2297,10 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li
school_name=school_name,
address=address,
price=price,
date=_utc_now_iso(),
software_name=software_name,
date=formatted_date,
software_name=software_name
)
except Exception as mail_err:
print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {mail_err}")
@@ -2302,10 +2317,10 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, package_li
"updated_at": _utc_now_iso()
}}
)
print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet fuer {subdomain}")
print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet für {subdomain}")
except Exception as e:
print(f"[ERROR] Async Provisioning fehlgeschlagen fuer {subdomain}:")
print(f"[ERROR] Async Provisioning fehlgeschlagen für {subdomain}:")
traceback.print_exc()
req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "failed", "last_error": str(e)}})
finally:
@@ -2350,7 +2365,6 @@ def booking_payment():
flow_config = BOOKING_FLOW_MAP[booking_flow]
# Auslesen aller Formularfelder (inkl. tenant_slug und Rechnungsdaten)
form_data = {
"contact_person": _sanitize_text(request.values.get("contact_person") or "", 120),
"school_name": _sanitize_text(request.values.get("school_name") or "", 160),
@@ -2426,21 +2440,8 @@ def booking_payment():
assigned_admin_display_name = assigned_admin.get("display_name") if assigned_admin else ""
assigned_admin_email = (assigned_admin.get("email") if assigned_admin else "") or BOOKING_ADMIN_EMAIL_FALLBACK
message = (
f"{flow_config['message_prefix']} {selected_package} {flow_config['message_suffix']}\n"
f"Ansprechpartner: {form_data['contact_person']}\n"
f"Schule: {form_data['school_name']} (Dienstst.-Nr: {form_data['school_number']})\n"
f"E-Mail: {form_data['contact_email']}\n"
f"Telefon: {form_data['contact_phone'] or '-'}\n"
f"Schulträger/Amt: {form_data['billing_organization']}\n"
f"Rechnungsadresse: {form_data['billing_street']}, {form_data['billing_zip']} {form_data['billing_city']}\n"
f"Kassenzeichen: {form_data['billing_reference'] or '-'}\n"
f"Gewünschte Subdomain: {form_data['tenant_slug']}\n"
f"Notizen: {form_data['notes'] or '-'}"
)
try:
# FLOW 1: Beratung / Gesprächs-Buchung
# FLOW 1: Consultation / Chat
if booking_flow == "consultation":
client, col = _get_collection("chat_messages")
try:
@@ -2449,7 +2450,7 @@ def booking_payment():
"username": session.get("username"),
"sender": session.get("display_name") or session.get("username"),
"sender_role": "user",
"message": message,
"message": f"Gesprächswunsch bezüglich: {selected_package}",
"created_at": _utc_now_iso(),
"booking_flow": booking_flow,
"booking_package": package_raw,
@@ -2465,18 +2466,16 @@ def booking_payment():
flash(f"Gesprächstermin für {selected_package} wurde an das Team gesendet.", "success")
return redirect(url_for("user_chat"))
# FLOW 2: Kostenpflichtige Direkt-Buchung mit Provisioning via Watcher
# FLOW 2: Direktbuchung mit Provisioning via Host-Watcher
if booking_flow == "payment":
booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
# Schul-URL aus dem Formular bevorzugen
raw_base = form_data.get("tenant_slug") or form_data.get("school_name") or session.get("username") or booking_number
base = _slugify_subdomain(raw_base).lower()[:51]
# Prüfung auf doppelte Subdomains via MongoDB 'instances' Collection
inst_check_client, inst_check_col = _get_collection("instances")
inst_check_client, inst_col_check = _get_collection("instances")
try:
existing_docs = list(inst_check_col.find({}, {"subdomain": 1}))
existing_docs = list(inst_col_check.find({}, {"subdomain": 1}))
existing_subdomains = [d.get("subdomain") for d in existing_docs if d.get("subdomain")]
except Exception as e:
print(f"[WARNING] Konnte bestehende Subdomains nicht prüfen: {e}")
@@ -2523,29 +2522,37 @@ def booking_payment():
username = session.get("username")
user_email = session.get("email") or form_data.get("contact_email")
address_process = f"{form_data['billing_street']}, {form_data['billing_zip']} {form_data['billing_city']}"
price_str = f"{total_price:.2f}".replace('.', ',')
# Startet den asynchronen Provisioning-Thread
# Startet den asynchronen Provisioning-Thread mit exakten Parametern
thread = threading.Thread(
target=background_booking_provisioning,
args=(app_obj, prov_id, subdomain, package_list, user_id, form_data["school_name"], user_email, username, address_process, total_price, selected_package)
args=(
app_obj,
prov_id,
subdomain,
package_list,
user_id,
form_data["school_name"],
user_email,
username,
address_process,
price_str,
selected_package
)
)
thread.start()
print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain}")
# ANPASSUNG FÜR BESSERE WARTEZEIT:
# Wir geben Metadaten mit, wie lange das Setup ca. dauern wird.
if is_json_request:
return jsonify({
"success": True,
"slug": subdomain,
"message": "Instanz wird eingerichtet...",
"estimated_seconds": 45, # Erwartete Dauer in Sekunden (anpassbar)
"status_poll_interval_ms": 3000 # Wie oft soll das Frontend fragen? (3 Sek)
"estimated_seconds": 45,
"status_poll_interval_ms": 3000
}), 200
# Falls es KEIN AJAX-Request war:
# Wir hängen den Parameter "wait_for_slug" an, damit die "my_instance_management"-Seite weiß,
# dass sie sofort ein Lade-Overlay für diesen spezifischen Tenant öffnen muss.
flash(f"Buchung für {selected_package} wurde erfasst. Deine Instanz wird jetzt automatisch eingerichtet.", "success")
return redirect(url_for("my_instance_management", wait_for_slug=subdomain))
+34 -30
View File
@@ -202,12 +202,11 @@ def generate_main_contract_pdf(
software_name: str = "Invario Inventarsystem"
):
"""Generiert den Hauptvertrag mit zentrierter und aufeinander abgestimmter Kopf- und Fußzeilen-Formatierung."""
from datetime import datetime
pdf = FPDF()
pdf.add_page()
pdf.set_auto_page_break(auto=True, margin=15)
# --- KOPFBEREICH (ZENTRIERT & BÜNDIG UNTEREINANDER) ---
# --- KOPFBEREICH ---
pdf.set_font("Helvetica", style="B", size=13)
pdf.cell(0, 6, "Vertrag über die Nutzung der Software-Lizenz", ln=True, align="C")
pdf.cell(0, 6, f'"{software_name}"', ln=True, align="C")
@@ -231,7 +230,7 @@ def generate_main_contract_pdf(
pdf.cell(0, 5, "- Auftraggeber / Schule -", ln=True, align="C")
pdf.ln(8)
# --- PARAGRAPHEN (1 BIS 8) ---
# --- PARAGRAPHEN ---
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "1. Vertragsgegenstand", ln=True)
pdf.set_font("Helvetica", size=10)
@@ -309,28 +308,25 @@ def generate_main_contract_pdf(
)
pdf.ln(6)
# --- FUSSBEREICH / UNTERSCHRIFTEN (ZENTRIERT & SYMMETRISCH) ---
# --- UNTERSCHRIFTEN ---
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "Unterschriften und Einverständnis", ln=True, align="C")
pdf.set_font("Helvetica", size=9)
pdf.multi_cell(0, 4, "Mit der Unterzeichnung bestätigen die Parteien den Abschluss des Hauptvertrages inklusive der beigefügten Anlagen (AGB, EULA und AVV).", align="C")
pdf.ln(8)
# Exakte Zentrierung der zwei Unterschriften-Blöcke
col_width = 85
start_x = (210 - (col_width * 2)) / 2 # Berechnet den exakten linken Rand für Symmetrie (20mm)
start_x = (210 - (col_width * 2)) / 2
pdf.set_font("Helvetica", size=9)
pdf.set_x(start_x)
pdf.cell(col_width, 5, "Für den Auftraggeber", align="C", ln=False)
pdf.cell(col_width, 5, "Für den Anbieter", align="C", ln=True)
pdf.ln(8)
pdf.set_x(start_x)
pdf.cell(col_width, 5, f"{datetime.today().strftime('%d.%m.%Y')} ________________________", align="C", ln=False)
pdf.cell(col_width, 5, f"{datetime.today().strftime('%d.%m.%Y')} ___________ _____________", align="C", ln=True)
pdf.cell(col_width, 5, f"{date} ________________________", align="C", ln=False)
pdf.cell(col_width, 5, f"{date} ___________ _____________", align="C", ln=True)
pdf.set_x(start_x)
pdf.cell(col_width, 5, "(Verantwortlicher)", align="C", ln=False)
@@ -339,27 +335,31 @@ def generate_main_contract_pdf(
pdf.output(output_path)
return output_path
def send_accreditation_email(
recipient: str,
domain: str,
username: str,
password: str,
invoice_pdf_path: str,
contract_pdf_path: str,
school_name: str = "[Name der Schule / des Auftraggebers]",
address: str = "[Straße, Hausnummer] [PLZ, Ort]",
price: str = "250,00",
date: str = "[Ort, Datum]"
date: str = "[Ort, Datum]",
software_name: str = "Invario Inventarsystem"
) -> bool:
"""Generiert den Hauptvertrag und sendet diesen zusammen mit Rechnung, AGB und AVV per E-Mail."""
"""Generiert den Hauptvertrag temporär und versendet ihn zusammen mit AGB & AVV per E-Mail."""
# 1. Hauptvertrag generieren
# Clean Sanitization für Dateinamen
safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip()
contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf"
contract_pdf_path = os.path.join(tempfile.gettempdir(), contract_filename)
# 1. Hauptvertrag dynamisch erzeugen
generate_main_contract_pdf(
school_name=school_name,
address=address,
price=price,
date=date,
software_name=software_name,
output_path=contract_pdf_path
)
@@ -367,13 +367,13 @@ def send_accreditation_email(
agb_pdf_path = os.path.join("static", "download", "AGB Invario.pdf")
avv_pdf_path = os.path.join("static", "download", "AVV Invario.pdf")
# 3. E-Mail Texte
# 3. E-Mail Inhalte
subject = "Ihre Zugangsdaten und Unterlagen für Invario"
text_note = (
f"Sehr geehrte/r {username},\n\n"
"Vielen Dank für Ihre Akkreditierung bei Invario. "
"Im Anhang finden Sie Ihre Rechnung, den Hauptvertrag sowie die AGB und den AVV.\n\n"
"Im Anhang finden Sie Ihren Hauptvertrag sowie die AGB und den AVV.\n\n"
f"Ihre Zugangsdaten:\n"
f"Domain: https://{domain}\n"
f"Benutzername: {username}\n"
@@ -385,7 +385,7 @@ def send_accreditation_email(
<div style="font-family: Arial, Helvetica, sans-serif; color: #333333;">
<h2 style="color: #2c3e50; margin-top: 0;">Willkommen bei Invario!</h2>
<p style="font-size: 15px; line-height: 1.6;">
Vielen Dank für Ihre Akkreditierung. Im Anhang finden Sie Ihre Rechnung, den Hauptvertrag sowie die AGB und den AVV.
Vielen Dank für Ihre Akkreditierung. Im Anhang finden Sie Ihren Hauptvertrag sowie die AGB und den AVV.
</p>
<h3 style="color: #2c3e50;">Ihre Zugangsdaten:</h3>
@@ -401,25 +401,29 @@ def send_accreditation_email(
</div>
"""
# 4. Alle existierenden Anhänge sammeln
# 4. Vorhandene Anhänge einsammeln
attachments = []
# Generierter Hauptvertrag & Rechnung
if contract_pdf_path and os.path.exists(contract_pdf_path):
if os.path.exists(contract_pdf_path):
attachments.append(contract_pdf_path)
if invoice_pdf_path and os.path.exists(invoice_pdf_path):
attachments.append(invoice_pdf_path)
# Statische Dokumente aus static/download/
if os.path.exists(agb_pdf_path):
attachments.append(agb_pdf_path)
else:
print(f"Warnung: AGB unter {agb_pdf_path} nicht gefunden.")
print(f"[WARNING] AGB unter {agb_pdf_path} nicht gefunden.")
if os.path.exists(avv_pdf_path):
attachments.append(avv_pdf_path)
else:
print(f"Warnung: AVV unter {avv_pdf_path} nicht gefunden.")
print(f"[WARNING] AVV unter {avv_pdf_path} nicht gefunden.")
# 5. E-Mail mit allen 4 Anhängen versenden
return send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
# 5. E-Mail versenden & temporäre Datei aufräumen
try:
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
return success
finally:
if os.path.exists(contract_pdf_path):
try:
os.remove(contract_pdf_path)
except OSError as e:
print(f"[WARNING] Konnte temporären Vertrag nicht löschen: {e}")