689 lines
29 KiB
Python
689 lines
29 KiB
Python
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from email.mime.application import MIMEApplication
|
|
import smtplib
|
|
import time
|
|
import os
|
|
import tempfile
|
|
from fpdf import FPDF
|
|
from flask import current_app
|
|
from pymongo import MongoClient
|
|
import threading
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
|
|
SELLER_INFO = {
|
|
"company_name": "Invario UG",
|
|
"sub_title": "Software Development",
|
|
"owner": "Invario Team",
|
|
"street": "Am Sportplatz 10",
|
|
"zip_city": "83052 Bruckmühl",
|
|
"email": "info@invario-software.de",
|
|
"website": "https://invario-software.de",
|
|
"bank_name": "Commerzbank AG",
|
|
"iban": "DE27 7114 0041 0183 3136 00",
|
|
"bic": "COBADEFFXXX",
|
|
"tax_number": "143/100/12345",
|
|
"vat_id": "HRB 35441"
|
|
}
|
|
|
|
def _build_smtp_client():
|
|
smtp = smtplib.SMTP(
|
|
"mail.invario-software.de",
|
|
587,
|
|
timeout=10,
|
|
)
|
|
smtp.ehlo()
|
|
if True: # Always use TLS for security
|
|
smtp.starttls()
|
|
smtp.ehlo()
|
|
smtp.login("no-reply@invario-software.de", "#,EATwIn,68")
|
|
return smtp
|
|
|
|
|
|
def send(
|
|
email: list | str,
|
|
subject: str,
|
|
text_body: str,
|
|
html_body: str = None,
|
|
attachments: list = None
|
|
) -> bool:
|
|
"""Sends the email with Plain Text, HTML, and optional File Attachments support."""
|
|
|
|
if isinstance(email, str):
|
|
email = [email]
|
|
|
|
if attachments is None:
|
|
attachments = []
|
|
|
|
TEXT_SIGNATURE = (
|
|
"\n\n--\n"
|
|
"Mit freundlichen Grüßen\n"
|
|
"Ihr Invario Team\n\n"
|
|
"Invario UG\n"
|
|
"Am Sportplatz 10\n"
|
|
"83052 Bruckmühl"
|
|
)
|
|
|
|
HTML_SIGNATURE = """
|
|
<br><br>
|
|
<table cellpadding="0" cellspacing="0" border="0" style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; color: #555555; line-height: 1.5; border-top: 1px solid #eaebed; padding-top: 15px; width: 100%;">
|
|
<tr>
|
|
<td>
|
|
<p style="margin:0 0 5px 0;">Mit freundlichen Grüßen,</p>
|
|
<p style="margin:0;"><strong style="font-size:15px; color: #333333;">Ihr Invario Team</strong></p><br>
|
|
<p style="margin:0 0 0 0; font-size: 12px; color: #888888;">
|
|
<strong>Invario UG</strong><br>
|
|
Am Sportplatz 10<br>
|
|
83052 Bruckmühl
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
"""
|
|
|
|
text_content = f"{text_body}{TEXT_SIGNATURE}"
|
|
|
|
if html_body:
|
|
html_content = f"<html><body style='background-color: #f9f9f9; padding: 20px;'><div style='background-color: #ffffff; padding: 30px; border-radius: 8px; max-width: 600px; margin: 0 auto; box-shadow: 0px 2px 5px rgba(0,0,0,0.05);'>{html_body}{HTML_SIGNATURE}</div></body></html>"
|
|
else:
|
|
# Fallback if only text is provided
|
|
html_safe_text = text_body.replace('\n', '<br>')
|
|
html_content = f"<html><body><p style='font-family: Arial, sans-serif; color: #333333;'>{html_safe_text}</p>{HTML_SIGNATURE}</body></html>"
|
|
|
|
mails_per_second = 10
|
|
interval = 1.0 / mails_per_second
|
|
smtp = None
|
|
|
|
try:
|
|
smtp = _build_smtp_client()
|
|
|
|
for i, recipient in enumerate(email):
|
|
start_time = time.time()
|
|
|
|
# Root message structure for email with attachments
|
|
msg = MIMEMultipart("mixed")
|
|
msg["Subject"] = subject
|
|
msg["From"] = "Invario Team <no-reply@invario-software.de>"
|
|
msg["To"] = str(recipient)
|
|
|
|
# Sub-container for Plain Text and HTML bodies
|
|
msg_body = MIMEMultipart("alternative")
|
|
msg_body.attach(MIMEText(text_content, "plain"))
|
|
msg_body.attach(MIMEText(html_content, "html"))
|
|
msg.attach(msg_body)
|
|
|
|
# Attach files if provided
|
|
for file_path in attachments:
|
|
if file_path and os.path.isfile(file_path):
|
|
filename = os.path.basename(file_path)
|
|
with open(file_path, "rb") as file:
|
|
part = MIMEApplication(file.read(), Name=filename)
|
|
part['Content-Disposition'] = f'attachment; filename="{filename}"'
|
|
msg.attach(part)
|
|
|
|
smtp.sendmail(
|
|
from_addr="no-reply@invario-software.de",
|
|
to_addrs=[recipient],
|
|
msg=msg.as_string()
|
|
)
|
|
|
|
elapsed_time = time.time() - start_time
|
|
sleep_time = interval - elapsed_time
|
|
|
|
if sleep_time > 0 and i < len(email) - 1:
|
|
time.sleep(sleep_time)
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f"Debug: Fehler beim Senden der E-Mail: {e}")
|
|
return False
|
|
finally:
|
|
if smtp:
|
|
try:
|
|
smtp.quit()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def send_register_token(email: str, token: str) -> bool:
|
|
"""Sends a professionally styled registration token to the user."""
|
|
|
|
subject = "Aktion erforderlich: Ihr Bestätigungscode für Invario"
|
|
|
|
text_note = (
|
|
"Herzlich willkommen bei Invario!\n\n"
|
|
"Um die Einrichtung Ihres Kontos abzuschließen und Ihre E-Mail-Adresse zu bestätigen, "
|
|
"geben Sie bitte den folgenden Sicherheitscode ein:\n\n"
|
|
f"Code: {token}\n\n"
|
|
"Dieser Code ist aus Sicherheitsgründen nur für begrenzte Zeit gültig. "
|
|
"Falls Sie diese Registrierung nicht angefordert haben, können Sie diese E-Mail einfach ignorieren."
|
|
)
|
|
|
|
html_note = f"""
|
|
<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 Registrierung. Um die Einrichtung Ihres Kontos abzuschließen und Ihre E-Mail-Adresse zu verifizieren, verwenden Sie bitte den folgenden Bestätigungscode:
|
|
</p>
|
|
|
|
<div style="background-color: #f1f5f9; border-left: 4px solid #3b82f6; border-radius: 4px; padding: 25px; text-align: center; margin: 30px 0;">
|
|
<span style="font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #1e293b;">{token}</span>
|
|
</div>
|
|
|
|
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">
|
|
Dieser Code ist aus Sicherheitsgründen nur für eine begrenzte Zeit gültig.<br>
|
|
Falls Sie kein Konto bei uns erstellt haben, können Sie diese E-Mail sicher ignorieren.
|
|
</p>
|
|
</div>
|
|
"""
|
|
|
|
return send(email, subject, text_body=text_note, html_body=html_note)
|
|
|
|
def send_password_reset_token(email: str, token: str) -> bool:
|
|
"""Sends a professionally styled password reset token to the user."""
|
|
subject = "Aktion erforderlich: Ihr Passwort-Zurücksetzungs-Code für Invario"
|
|
|
|
text_note = (
|
|
"Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.\n\n"
|
|
"Um Ihr Passwort zurückzusetzen, geben Sie bitte den folgenden Sicherheitscode ein:\n\n"
|
|
f"Code: {token}\n\n"
|
|
"Dieser Code ist aus Sicherheitsgründen nur für begrenzte Zeit gültig. "
|
|
"Falls Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail einfach ignorieren."
|
|
)
|
|
|
|
html_note = f"""
|
|
<div style="font-family: Arial, Helvetica, sans-serif; color: #333333;">
|
|
<h2 style="color: #2c3e50; margin-top: 0;">Passwort zurücksetzen</h2>
|
|
<p style="font-size: 15px; line-height: 1.6;">
|
|
Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Um fortzufahren, verwenden Sie bitte den folgenden Bestätigungscode:
|
|
</p>
|
|
|
|
<div style="background-color: #f1f5f9; border-left: 4px solid #3b82f6; border-radius: 4px; padding: 25px; text-align: center; margin: 30px 0;">
|
|
<span style="font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #1e293b;">{token}</span>
|
|
</div>
|
|
|
|
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">
|
|
Dieser Code ist aus Sicherheitsgründen nur für eine begrenzte Zeit gültig.<br>
|
|
Falls Sie kein Zurücksetzen des Passworts angefordert haben, können Sie diese E-Mail sicher ignorieren.
|
|
</p>
|
|
</div>
|
|
"""
|
|
|
|
return send(email, subject, text_body=text_note, html_body=html_note)
|
|
|
|
def generate_main_contract_pdf(
|
|
school_name: str,
|
|
address: str,
|
|
price: str,
|
|
date: str,
|
|
output_path: str,
|
|
software_name: str = "Invario Inventarsystem"
|
|
):
|
|
"""Generiert den Hauptvertrag mit zentrierter und aufeinander abgestimmter Kopf- und Fußzeilen-Formatierung."""
|
|
pdf = FPDF()
|
|
pdf.add_page()
|
|
pdf.set_auto_page_break(auto=True, margin=15)
|
|
|
|
# --- 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")
|
|
pdf.ln(6)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 5, "Zwischen", ln=True, align="C")
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.cell(0, 5, "Invario UG", ln=True, align="C")
|
|
pdf.cell(0, 5, "Am Sportplatz 10, 83052 Bruckmühl", ln=True, align="C")
|
|
pdf.set_font("Helvetica", style="I", size=9)
|
|
pdf.cell(0, 5, "- Anbieter / Lizenzgeber -", ln=True, align="C")
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 5, "und", ln=True, align="C")
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.cell(0, 5, f"{school_name}", ln=True, align="C")
|
|
pdf.cell(0, 5, f"{address}", ln=True, align="C")
|
|
pdf.set_font("Helvetica", style="I", size=9)
|
|
pdf.cell(0, 5, "- Auftraggeber / Schule -", ln=True, align="C")
|
|
pdf.ln(8)
|
|
|
|
# --- PARAGRAPHEN ---
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "1. Vertragsgegenstand", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
f'(1) Der Anbieter stellt dem Auftraggeber die Software "{software_name}" (nachfolgend "Software") als webbasierte Anwendung im Wege des Software-as-a-Service (SaaS) zur Verfügung. Die Software wird auf der vom Anbieter genutzten Infrastruktur betrieben.\n'
|
|
"(2) Dieser Hauptvertrag wird durch folgende Dokumente ergänzt, die als integrale Bestandteile dieses Vertrages gelten:\n"
|
|
"a) Anlage 1: Allgemeine Geschäftsbedingungen (AGB) und Lizenzbedingungen (EULA) des Anbieters\n"
|
|
"b) Anlage 2: Auftragsverarbeitungsvertrag (AVV) gemäß Art. 28 DS-GVO nebst technischen und organisatorischen Maßnahmen (TOM)"
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "2. Vertragsbeginn", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
"(1) Dieser Vertrag kommt entweder mit dem Datum der beidseitigen Unterzeichnung dieses Dokumentes oder durch die Nutzung der integrierten digitalen Bestellfunktion im Invario-Portal durch einen hierzu autorisierten Vertreter des Auftraggebers zustande.\n"
|
|
"(2) Die Vertragslaufzeit beginnt mit der Bereitstellung der Zugangsdaten durch den Anbieter."
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "3. Laufzeit und Kündigung", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
"(1) Der Vertrag wird zunächst für die Dauer von zwölf (12) Monaten geschlossen (Mindestlaufzeit).\n"
|
|
"(2) Er verlängert sich automatisch um jeweils ein weiteres Jahr, sofern er nicht von einer Partei mit einer Frist von drei (3) Monaten zum Ende der jeweiligen Laufzeit gekündigt wird.\n"
|
|
"(3) Die Kündigung kann in Textform (z. B. per E-Mail) oder über die dafür vorgesehene Funktion im Invario-Portal erfolgen. Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt."
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "4. Vergütung und Bezahlung", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
f"(1) Für die Nutzung der Software zahlt der Auftraggeber eine jährliche Lizenzgebühr in Höhe von {price} EUR (zzgl. ggf. anfallender gesetzlicher Umsatzsteuer).\n"
|
|
"(2) Die Vergütung ist jeweils für ein Vertragsjahr im Voraus fällig.\n"
|
|
"(3) Der Rechnungsbetrag ist innerhalb von 30 Tagen nach Erhalt der Rechnung ohne Abzug zur Zahlung fällig."
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "5. Nutzung und Support", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
"(1) Der Umfang der Lizenzeinräumung sowie die Nutzungsbeschränkungen richten sich nach den Bestimmungen der anliegenden EULA (Anlage 1).\n"
|
|
"(2) Der technische Support wird innerhalb der in den AGB (Anlage 1) definierten Servicezeiten über das integrierte Ticket-System oder per E-Mail erbracht."
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "6. Datenschutz und Datensicherheit", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
"(1) Die Parteien verpflichten sich zur Einhaltung der datenschutzrechtlichen Bestimmungen. Die Details der Verarbeitung der personenbezogenen Daten werden im anliegenden AVV (Anlage 2) verbindlich geregelt.\n"
|
|
"(2) Der Anbieter sichert zu, dass die Speicherung und Verarbeitung der Daten des Auftraggebers ausschließlich in Rechenzentren mit Standort innerhalb der Bundesrepublik Deutschland erfolgt."
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "7. Haftung", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
"(1) Die Haftung des Anbieters richtet sich nach den Regelungen in Ziffer 7 der Allgemeinen Geschäftsbedingungen (Anlage 1).\n"
|
|
"(2) Für Unterbrechungen der Verfügbarkeit durch höhere Gewalt, Störungen im Bereich außerhalb des Übergabepunktes (z. B. Internet-Backbone) sowie für unvorhersehbare Sicherheitslücken, die nach dem aktuellen Stand der Technik zum Zeitpunkt des Auftretens noch nicht abwehrbar waren (Zero-Day-Exploits), haftet der Anbieter nur nach Maßgabe der Haftungsregelungen in den AGB, es sei denn, ihn trifft ein Verschulden an der verzögerten Behebung. Gesetzliche Datenschutzpflichten und Meldepflichten bleiben unberührt."
|
|
)
|
|
pdf.ln(4)
|
|
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 6, "8. Schlussbestimmungen", ln=True)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.multi_cell(0, 5,
|
|
"(1) Änderungen und Ergänzungen dieses Vertrages bedürfen der Textform.\n"
|
|
"(2) Sollten einzelne Bestimmungen dieses Vertrages unwirksam oder undurchführbar sein oder werden, bleibt die Wirksamkeit der übrigen Bestimmungen hiervon unberührt. Anstelle der unwirksamen oder undurchführbaren Bestimmung gelten die gesetzlichen Vorschriften. Sofern es sich nicht um Allgemeine Geschäftsbedingungen handelt, verpflichten sich die Parteien, eine wirksame Regelung zu vereinbaren, die dem wirtschaftlichen Zweck der unwirksamen oder undurchführbaren Bestimmung möglichst nahekommt.\n"
|
|
"(3) Es gilt das Recht der Bundesrepublik Deutschland. Ausschließlicher Gerichtsstand für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertrag ist, soweit gesetzlich zulässig, der Sitz des Anbieters."
|
|
)
|
|
pdf.ln(6)
|
|
|
|
# --- 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)
|
|
|
|
col_width = 85
|
|
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"{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)
|
|
pdf.cell(col_width, 5, "(Simon Brieger)", align="C", ln=True)
|
|
|
|
pdf.output(output_path)
|
|
return output_path
|
|
|
|
def generate_invoice_fpdf(
|
|
school_name: str,
|
|
address: str,
|
|
price: str,
|
|
date: str,
|
|
software_name: str,
|
|
output_path: str,
|
|
invoice_number: str = "RE-2026-08-001"
|
|
):
|
|
"""Erstellt eine rechtssichere Rechnung inkl. Dankessatz und SELLER_INFO Daten mittels FPDF2."""
|
|
|
|
# 1. Beträge berechnen (Annahme: übergebener Preis ist Brutto)
|
|
try:
|
|
brutto = float(price.replace('.', '').replace(',', '.'))
|
|
except ValueError:
|
|
brutto = 250.00
|
|
|
|
netto = brutto / 1.19
|
|
mwst = brutto - netto
|
|
|
|
pdf = FPDF()
|
|
pdf.add_page()
|
|
|
|
# 2. Kopfzeile: Aussteller-Informationen aus SELLER_INFO (Rechtsbündig)
|
|
pdf.set_font("Helvetica", size=9)
|
|
pdf.cell(0, 5, f"{SELLER_INFO['company_name']} - {SELLER_INFO['sub_title']}", new_x="LMARGIN", new_y="NEXT", align="R")
|
|
pdf.cell(0, 5, SELLER_INFO['street'], new_x="LMARGIN", new_y="NEXT", align="R")
|
|
pdf.cell(0, 5, SELLER_INFO['zip_city'], new_x="LMARGIN", new_y="NEXT", align="R")
|
|
pdf.cell(0, 5, f"St.-Nr.: {SELLER_INFO['tax_number']} | USt-IdNr.: {SELLER_INFO['vat_id']}", new_x="LMARGIN", new_y="NEXT", align="R")
|
|
pdf.ln(10)
|
|
|
|
# 3. Empfänger-Informationen (Linksbündig)
|
|
pdf.set_font("Helvetica", size=11)
|
|
pdf.cell(0, 5, school_name, new_x="LMARGIN", new_y="NEXT")
|
|
pdf.cell(0, 5, address, new_x="LMARGIN", new_y="NEXT")
|
|
pdf.ln(15)
|
|
|
|
# 4. Rechnungs-Metadaten
|
|
pdf.set_font("Helvetica", style="B", size=16)
|
|
pdf.cell(0, 10, "Rechnung", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.cell(0, 5, f"Rechnungsnummer: {invoice_number}", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.cell(0, 5, f"Rechnungsdatum: {date}", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.cell(0, 5, "Liefer-/Leistungszeitraum: Entspricht Rechnungsdatum", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.ln(8)
|
|
|
|
# Netter Dankessatz direkt über der Tabelle
|
|
pdf.set_font("Helvetica", style="I", size=10)
|
|
pdf.cell(0, 6, "Vielen Dank für Ihren Einkauf! Wir freuen uns sehr über das entgegengebrachte Vertrauen.", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.ln(6)
|
|
|
|
# 5. Positionen (Tabellenkopf)
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(20, 8, "Menge", border=1)
|
|
pdf.cell(100, 8, "Bezeichnung / Art der Leistung", border=1)
|
|
pdf.cell(35, 8, "Einzelpreis (Netto)", border=1, align="R")
|
|
pdf.cell(35, 8, "Gesamt (Netto)", border=1, new_x="LMARGIN", new_y="NEXT", align="R")
|
|
|
|
# Positionen (Zeile)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.cell(20, 8, "1", border=1)
|
|
pdf.cell(100, 8, f"Nutzungs Lizenz für {software_name}", border=1)
|
|
pdf.cell(35, 8, f"{netto:.2f} EUR", border=1, align="R")
|
|
pdf.cell(35, 8, f"{netto:.2f} EUR", border=1, new_x="LMARGIN", new_y="NEXT", align="R")
|
|
pdf.ln(5)
|
|
|
|
# 6. Summenblock
|
|
pdf.set_left_margin(120) # Einrücken für die Summen
|
|
pdf.cell(35, 8, "Summe Netto:", align="R")
|
|
pdf.cell(35, 8, f"{netto:.2f} EUR", new_x="LMARGIN", new_y="NEXT", align="R")
|
|
|
|
pdf.cell(35, 8, "zzgl. 19% USt.:", align="R")
|
|
pdf.cell(35, 8, f"{mwst:.2f} EUR", new_x="LMARGIN", new_y="NEXT", align="R")
|
|
|
|
pdf.cell(35, 8, "Abzüglich Skonto:", align="R")
|
|
pdf.cell(35, 8, "0.00 EUR", new_x="LMARGIN", new_y="NEXT", align="R")
|
|
|
|
pdf.set_font("Helvetica", style="B", size=11)
|
|
pdf.cell(35, 10, "Gesamtbetrag (Brutto):", align="R")
|
|
pdf.cell(35, 10, f"{brutto:.2f} EUR", new_x="LMARGIN", new_y="NEXT", align="R")
|
|
|
|
# 7. Zahlungsbedingungen, Bankverbindung & Footer
|
|
pdf.set_left_margin(10) # Rand zurücksetzen
|
|
pdf.ln(10)
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.cell(0, 5, "Bitte überweisen Sie den fälligen Bruttobetrag innerhalb von 14 Tagen unter Angabe der", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.cell(0, 5, f"Rechnungsnummer ({invoice_number}) auf unser unten angegebenes Bankkonto. Es wurde kein Skonto vereinbart.", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.ln(5)
|
|
|
|
# Bankinformationen Block aus SELLER_INFO
|
|
pdf.set_font("Helvetica", style="B", size=10)
|
|
pdf.cell(0, 5, "Bankverbindung für Ihre Überweisung:", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.set_font("Helvetica", size=10)
|
|
pdf.cell(0, 5, f"Bankinstitut: {SELLER_INFO['bank_name']}", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.cell(0, 5, f"IBAN: {SELLER_INFO['iban']}", new_x="LMARGIN", new_y="NEXT")
|
|
pdf.cell(0, 5, f"BIC: {SELLER_INFO['bic']}", new_x="LMARGIN", new_y="NEXT")
|
|
|
|
# Speichern
|
|
pdf.output(output_path)
|
|
|
|
|
|
def send_accreditation_email(
|
|
recipient: str,
|
|
domain: str,
|
|
username: str,
|
|
password: str,
|
|
user_id: str,
|
|
school_name: str = "[Name der Schule / des Auftraggebers]",
|
|
address: str = "[Straße, Hausnummer] [PLZ, Ort]",
|
|
price: str = "250,00",
|
|
date: str = "[Ort, Datum]",
|
|
software_name: str = "Invario Inventarsystem",
|
|
invoice_path: str = None
|
|
) -> bool:
|
|
"""Generiert Hauptvertrag und Rechnung temporär als PDF und versendet sie."""
|
|
|
|
safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip()
|
|
|
|
# Dynamische Rechnungsnummer generieren (z.B. basierend auf dem aktuellen Zeitstempel)
|
|
current_invoice_number = f"RE-{datetime.now().strftime('%Y%m%d-%H%M')}"
|
|
|
|
# 1. Hauptvertrag dynamisch erzeugen
|
|
contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf"
|
|
contract_pdf_path = os.path.join(tempfile.gettempdir(), contract_filename)
|
|
|
|
generate_main_contract_pdf(
|
|
school_name=school_name,
|
|
address=address,
|
|
price=price,
|
|
date=date,
|
|
software_name=software_name,
|
|
output_path=contract_pdf_path
|
|
)
|
|
|
|
# 1b. Rechnung dynamisch mit FPDF erzeugen inkl. SELLER_INFO
|
|
generated_invoice_path = None
|
|
if not invoice_path:
|
|
invoice_filename = f"Rechnung_Invario_{safe_school_name}.pdf"
|
|
generated_invoice_path = os.path.join(tempfile.gettempdir(), invoice_filename)
|
|
|
|
generate_invoice_fpdf(
|
|
school_name=school_name,
|
|
address=address,
|
|
price=price,
|
|
date=date,
|
|
software_name=software_name,
|
|
output_path=generated_invoice_path,
|
|
invoice_number=current_invoice_number
|
|
)
|
|
invoice_to_attach = generated_invoice_path
|
|
else:
|
|
invoice_to_attach = invoice_path
|
|
|
|
if current_app:
|
|
static_dir = current_app.static_folder
|
|
else:
|
|
static_dir = os.path.join(os.getcwd(), "static")
|
|
|
|
def get_valid_pdf_path(filename):
|
|
possible_paths = [
|
|
os.path.join(static_dir, "downloads", filename),
|
|
os.path.join(static_dir, "download", filename),
|
|
os.path.join("/opt/Key-service-Server/Website/static/downloads", filename),
|
|
os.path.join("/opt/Key-service-Server/Website/static/download", filename),
|
|
]
|
|
for path in possible_paths:
|
|
if os.path.exists(path):
|
|
return path
|
|
return None
|
|
|
|
agb_pdf_path = get_valid_pdf_path("AGB Invario.pdf")
|
|
avv_pdf_path = get_valid_pdf_path("AVV Invario.pdf")
|
|
|
|
# 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 Unterlagen (Hauptvertrag, Rechnung, AGB und AVV).\n\n"
|
|
f"Ihre Zugangsdaten:\n"
|
|
f"Domain: https://{domain}\n"
|
|
f"Benutzername: {username}\n"
|
|
f"Passwort: {password}\n\n"
|
|
"Bitte bewahren Sie diese Informationen sicher auf."
|
|
)
|
|
|
|
html_note = f"""
|
|
<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 Ihren Hauptvertrag, Ihre Rechnung sowie AGB und AVV.
|
|
Bitte laden Sie die Dokumente herunter und unterschreiben Sie den Hauptvertrag. Danach können Sie die Unterlagen per E-Mail an uns zurücksenden oder über das Invario-Portal hochladen, um die Akkreditierung abzuschließen.
|
|
</p>
|
|
|
|
<h3 style="color: #2c3e50;">Ihre Zugangsdaten:</h3>
|
|
<ul style="font-size: 15px; line-height: 1.6;">
|
|
<li><strong>Domain:</strong> <a href="https://{domain}">https://{domain}</a></li>
|
|
<li><strong>Benutzername:</strong> {username}</li>
|
|
<li><strong>Passwort:</strong> {password}</li>
|
|
</ul>
|
|
|
|
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">
|
|
Bitte bewahren Sie diese Informationen sicher auf.
|
|
</p>
|
|
</div>
|
|
"""
|
|
|
|
# 4. Vorhandene Anhänge sammeln
|
|
attachments = []
|
|
|
|
if os.path.exists(contract_pdf_path):
|
|
attachments.append(contract_pdf_path)
|
|
|
|
if invoice_to_attach and os.path.exists(invoice_to_attach):
|
|
attachments.append(invoice_to_attach)
|
|
|
|
if agb_pdf_path:
|
|
attachments.append(agb_pdf_path)
|
|
|
|
if avv_pdf_path:
|
|
attachments.append(avv_pdf_path)
|
|
|
|
|
|
# ---------- MONGO DB SETUP ----------
|
|
def _env_int(name: str, default: int) -> int:
|
|
value = os.environ.get(name)
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
|
|
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
|
|
MONGO_MAX_POOL_SIZE = max(_env_int("MONGO_MAX_POOL_SIZE", 12), 1)
|
|
MONGO_MIN_POOL_SIZE = max(_env_int("MONGO_MIN_POOL_SIZE", 0), 0)
|
|
MONGO_MAX_IDLE_MS = max(_env_int("MONGO_MAX_IDLE_MS", 60000), 1000)
|
|
MONGO_CONNECT_TIMEOUT_MS = max(_env_int("MONGO_CONNECT_TIMEOUT_MS", 1500), 500)
|
|
MONGO_SOCKET_TIMEOUT_MS = max(_env_int("MONGO_SOCKET_TIMEOUT_MS", 30000), 1000)
|
|
MONGO_WAIT_QUEUE_TIMEOUT_MS = max(_env_int("MONGO_WAIT_QUEUE_TIMEOUT_MS", 2000), 500)
|
|
|
|
global _MONGO_CLIENT, _MONGO_LOCK
|
|
if '_MONGO_CLIENT' not in globals():
|
|
_MONGO_CLIENT = None
|
|
_MONGO_LOCK = threading.Lock()
|
|
|
|
class _NoopMongoClientHandle:
|
|
def close(self):
|
|
return None
|
|
|
|
def _get_mongo_client() -> MongoClient:
|
|
global _MONGO_CLIENT
|
|
if _MONGO_CLIENT is not None:
|
|
return _MONGO_CLIENT
|
|
|
|
with _MONGO_LOCK:
|
|
if _MONGO_CLIENT is None:
|
|
_MONGO_CLIENT = MongoClient(
|
|
MONGO_URI,
|
|
serverSelectionTimeoutMS=MONGO_CONNECT_TIMEOUT_MS,
|
|
connectTimeoutMS=MONGO_CONNECT_TIMEOUT_MS,
|
|
socketTimeoutMS=MONGO_SOCKET_TIMEOUT_MS,
|
|
maxPoolSize=MONGO_MAX_POOL_SIZE,
|
|
minPoolSize=MONGO_MIN_POOL_SIZE,
|
|
maxIdleTimeMS=MONGO_MAX_IDLE_MS,
|
|
waitQueueTimeoutMS=MONGO_WAIT_QUEUE_TIMEOUT_MS,
|
|
)
|
|
return _MONGO_CLIENT
|
|
|
|
def _get_mongo_db():
|
|
client = _get_mongo_client()
|
|
return _NoopMongoClientHandle(), client[MONGO_DB_NAME]
|
|
|
|
def _get_collection(name: str):
|
|
client, db = _get_mongo_db()
|
|
return client, db[name]
|
|
|
|
def _utc_now_iso():
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
# Berechne das Fälligkeitsdatum (14 Tage ab heute)
|
|
computed_due_date = (datetime.now(timezone.utc) + timedelta(days=14)).isoformat()
|
|
|
|
# Collections abrufen
|
|
req_client, req_col = _get_collection("instance_requests")
|
|
inv_client, col = _get_collection("invoices") # Collection für Rechnungen initialisieren
|
|
|
|
# 1. Die neue Rechnung in die Datenbank (invoices-Collection) schreiben
|
|
# Wir nutzen hier die übergebenen Funktionsparameter anstatt des undefinierten "prov"
|
|
col.insert_one(
|
|
{
|
|
"username": user_id,
|
|
"school_name": school_name,
|
|
"address": address,
|
|
"invoice_number": current_invoice_number,
|
|
"period": date,
|
|
"amount_eur": price,
|
|
"status": "Zu prüfen",
|
|
"due_date": computed_due_date,
|
|
"pdf_path": invoice_to_attach,
|
|
"created_at": _utc_now_iso(),
|
|
"source": "booking_payment",
|
|
"software_name": software_name,
|
|
"domain": domain
|
|
}
|
|
)
|
|
|
|
req_col.update_one(
|
|
{"username": user_id},
|
|
{"$set": {
|
|
"provision_status": "invoice_created",
|
|
"updated_at": _utc_now_iso()
|
|
}}
|
|
)
|
|
|
|
# 5. E-Mail versenden & temporäre Dateien 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:
|
|
pass
|
|
|
|
if generated_invoice_path and os.path.exists(generated_invoice_path):
|
|
try:
|
|
os.remove(generated_invoice_path)
|
|
except OSError:
|
|
pass |