adddition for the rechunung and the change for the HRB in the Impressum
This commit is contained in:
@@ -7,8 +7,24 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
from fpdf import FPDF
|
from fpdf import FPDF
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from weasyprint import HTML
|
||||||
|
|
||||||
|
|
||||||
|
SELLER_INFO = {
|
||||||
|
"company_name": "Invario UG",
|
||||||
|
"sub_title": "Inventar- & Schulsoftware",
|
||||||
|
"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():
|
def _build_smtp_client():
|
||||||
smtp = smtplib.SMTP(
|
smtp = smtplib.SMTP(
|
||||||
"mail.invario-software.de",
|
"mail.invario-software.de",
|
||||||
@@ -336,6 +352,458 @@ def generate_main_contract_pdf(
|
|||||||
pdf.output(output_path)
|
pdf.output(output_path)
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
|
def generate_invoice_pdf(
|
||||||
|
invoice_number: str,
|
||||||
|
invoice_date: str,
|
||||||
|
due_date: str,
|
||||||
|
customer: dict,
|
||||||
|
seller: dict,
|
||||||
|
items: list,
|
||||||
|
output_path: str,
|
||||||
|
notes: str = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generiert eine professionelle PDF-Rechnung mit WeasyPrint.
|
||||||
|
|
||||||
|
:param invoice_number: Rechnungsnummer (z. B. "INV-20260827-100002")
|
||||||
|
:param invoice_date: Rechnungsdatum (z. B. "27.08.2026")
|
||||||
|
:param due_date: Fälligkeitsdatum (z. B. "10.09.2026")
|
||||||
|
:param customer: Dict mit Kundeninformationen (name, attn, street, zip_city, customer_id)
|
||||||
|
:param seller: Dict mit Verkäuferinformationen (company_name, street, zip_city, iban, bic, vat_id, etc.)
|
||||||
|
:param items: Liste von Dicts mit Leistungspositionen (description, details, quantity, unit_price, tax_rate)
|
||||||
|
:param output_path: Zielpfad für die generierte PDF-Datei
|
||||||
|
:param notes: Optionaler Hinweistext (z. B. Zahlungsbedingungen oder Kleinunternehmerregelung)
|
||||||
|
"""
|
||||||
|
# 1. Summen und Steuern berechnen
|
||||||
|
subtotal = 0.0
|
||||||
|
tax_totals = {}
|
||||||
|
|
||||||
|
processed_items = []
|
||||||
|
for item in items:
|
||||||
|
qty = item.get("quantity", 1)
|
||||||
|
unit_price = item.get("unit_price", 0.0)
|
||||||
|
tax_rate = item.get("tax_rate", 19.0)
|
||||||
|
|
||||||
|
line_total = qty * unit_price
|
||||||
|
subtotal += line_total
|
||||||
|
|
||||||
|
tax_amount = line_total * (tax_rate / 100.0)
|
||||||
|
tax_totals[tax_rate] = tax_totals.get(tax_rate, 0.0) + tax_amount
|
||||||
|
|
||||||
|
processed_items.append({
|
||||||
|
"pos": len(processed_items) + 1,
|
||||||
|
"description": item.get("description", ""),
|
||||||
|
"details": item.get("details", ""),
|
||||||
|
"quantity": qty,
|
||||||
|
"unit_price": f"{unit_price:,.2f}".replace(",", "X").replace(".", ",").replace("X", "."),
|
||||||
|
"tax_rate": f"{tax_rate:.0f}%" if tax_rate.is_integer() else f"{tax_rate:.1f}%",
|
||||||
|
"line_total": f"{line_total:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
})
|
||||||
|
|
||||||
|
total_tax = sum(tax_totals.values())
|
||||||
|
grand_total = subtotal + total_tax
|
||||||
|
|
||||||
|
formatted_subtotal = f"{subtotal:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
formatted_grand_total = f"{grand_total:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
|
||||||
|
# Tabellenzeilen für Leistungen erzeugen
|
||||||
|
items_html = ""
|
||||||
|
for item in processed_items:
|
||||||
|
details_html = f'<div class="item-details">{item["details"]}</div>' if item["details"] else ""
|
||||||
|
items_html += f"""
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{item['pos']}</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-title">{item['description']}</div>
|
||||||
|
{details_html}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{item['quantity']}</td>
|
||||||
|
<td class="text-right">{item['unit_price']} €</td>
|
||||||
|
<td class="text-center">{item['tax_rate']}</td>
|
||||||
|
<td class="text-right">{item['line_total']} €</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Steuer-Aufschlüsselung erzeugen
|
||||||
|
tax_breakdown_html = ""
|
||||||
|
for rate, amt in tax_totals.items():
|
||||||
|
rate_str = f"{rate:.0f}%" if rate.is_integer() else f"{rate:.1f}%"
|
||||||
|
amt_str = f"{amt:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
tax_breakdown_html += f"""
|
||||||
|
<tr>
|
||||||
|
<td class="summary-label">zzgl. MwSt. ({rate_str}):</td>
|
||||||
|
<td class="summary-value">{amt_str} €</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
notes_html = f'<div class="notes-box"><strong>Hinweis:</strong> {notes}</div>' if notes else ""
|
||||||
|
|
||||||
|
# HTML & CSS Template
|
||||||
|
html_content = f"""<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rechnung {invoice_number}</title>
|
||||||
|
<style>
|
||||||
|
@page {{
|
||||||
|
size: A4;
|
||||||
|
margin: 15mm 15mm 20mm 15mm;
|
||||||
|
@bottom-right {{
|
||||||
|
content: "Seite " counter(page) " von " counter(pages);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #64748b;
|
||||||
|
}}
|
||||||
|
@bottom-left {{
|
||||||
|
content: "{seller.get('company_name', '')} • {seller.get('website', '')}";
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #64748b;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
*, *::before, *::after {{
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.header-table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}}
|
||||||
|
.header-table td {{ vertical-align: top; }}
|
||||||
|
.logo-title {{
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0f172a;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}}
|
||||||
|
.subtitle {{
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}}
|
||||||
|
.seller-info-top {{
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #64748b;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.meta-table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}}
|
||||||
|
.meta-table td {{ vertical-align: top; }}
|
||||||
|
.sender-line {{
|
||||||
|
font-size: 7.5pt;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}}
|
||||||
|
.recipient-address {{
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #0f172a;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.invoice-details-card {{
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
width: 230px;
|
||||||
|
float: right;
|
||||||
|
}}
|
||||||
|
.invoice-details-table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
}}
|
||||||
|
.invoice-details-table td {{ padding: 3px 0; }}
|
||||||
|
.invoice-details-table .label {{ color: #64748b; }}
|
||||||
|
.invoice-details-table .value {{
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
color: #0f172a;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.doc-title {{
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}}
|
||||||
|
.doc-intro {{
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.items-table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
.items-table th {{
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}}
|
||||||
|
.items-table td {{
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
font-size: 9pt;
|
||||||
|
vertical-align: top;
|
||||||
|
}}
|
||||||
|
.items-table tr:nth-child(even) td {{ background-color: #f8fafc; }}
|
||||||
|
.item-title {{ font-weight: bold; color: #0f172a; }}
|
||||||
|
.item-details {{ font-size: 8.5pt; color: #64748b; margin-top: 3px; }}
|
||||||
|
|
||||||
|
.text-center {{ text-align: center; }}
|
||||||
|
.text-right {{ text-align: right; }}
|
||||||
|
|
||||||
|
.summary-wrapper {{
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}}
|
||||||
|
.summary-table {{
|
||||||
|
width: 280px;
|
||||||
|
margin-left: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
}}
|
||||||
|
.summary-table td {{ padding: 4px 8px; }}
|
||||||
|
.summary-label {{ text-align: right; color: #64748b; }}
|
||||||
|
.summary-value {{ text-align: right; font-weight: bold; color: #0f172a; }}
|
||||||
|
.grand-total-row td {{
|
||||||
|
border-top: 2px solid #0f172a;
|
||||||
|
border-bottom: 2px double #0f172a;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-size: 11pt;
|
||||||
|
}}
|
||||||
|
.grand-total-row .summary-label {{ color: #0f172a; font-weight: bold; }}
|
||||||
|
.grand-total-row .summary-value {{ color: #2563eb; font-weight: bold; }}
|
||||||
|
|
||||||
|
.notes-box {{
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
border-left: 3px solid #2563eb;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.payment-info {{
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.footer-grid {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-top: 1px solid #cbd5e1;
|
||||||
|
padding-top: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}}
|
||||||
|
.footer-grid td {{
|
||||||
|
width: 33.33%;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}}
|
||||||
|
.footer-heading {{
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<table class="header-table">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="logo-title">{seller.get('company_name', 'INVARIO')}</div>
|
||||||
|
<div class="subtitle">{seller.get('sub_title', 'Software & Inventarsysteme')}</div>
|
||||||
|
</td>
|
||||||
|
<td class="seller-info-top">
|
||||||
|
<strong>{seller.get('company_name', '')}</strong><br>
|
||||||
|
{seller.get('street', '')}<br>
|
||||||
|
{seller.get('zip_city', '')}<br>
|
||||||
|
E-Mail: {seller.get('email', '')}<br>
|
||||||
|
Web: {seller.get('website', '')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 55%;">
|
||||||
|
<div class="sender-line">
|
||||||
|
{seller.get('company_name', '')} • {seller.get('street', '')} • {seller.get('zip_city', '')}
|
||||||
|
</div>
|
||||||
|
<div class="recipient-address">
|
||||||
|
<strong>{customer.get('name', '')}</strong><br>
|
||||||
|
{f"z. Hd. {customer['attn']}<br>" if customer.get('attn') else ""}
|
||||||
|
{customer.get('street', '')}<br>
|
||||||
|
{customer.get('zip_city', '')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="width: 45%;">
|
||||||
|
<div class="invoice-details-card">
|
||||||
|
<table class="invoice-details-table">
|
||||||
|
<tr>
|
||||||
|
<td class="label">Rechnungs-Nr.:</td>
|
||||||
|
<td class="value">{invoice_number}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Datum:</td>
|
||||||
|
<td class="value">{invoice_date}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Fällig am:</td>
|
||||||
|
<td class="value">{due_date}</td>
|
||||||
|
</tr>
|
||||||
|
{f'<tr><td class="label">Kunden-Nr.:</td><td class="value">{customer.get("customer_id")}</td></tr>' if customer.get("customer_id") else ''}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="doc-title">Rechnung {invoice_number}</div>
|
||||||
|
<div class="doc-intro">
|
||||||
|
Vielen Dank für Ihren Auftrag. Wir stellen Ihnen die nachfolgend aufgeführten Leistungen in Rechnung:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 6%;" class="text-center">Pos.</th>
|
||||||
|
<th style="width: 48%;">Beschreibung</th>
|
||||||
|
<th style="width: 8%;" class="text-center">Menge</th>
|
||||||
|
<th style="width: 14%;" class="text-right">Einzelpreis</th>
|
||||||
|
<th style="width: 10%;" class="text-center">MwSt.</th>
|
||||||
|
<th style="width: 14%;" class="text-right">Gesamt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items_html}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="summary-wrapper">
|
||||||
|
<table class="summary-table">
|
||||||
|
<tr>
|
||||||
|
<td class="summary-label">Zwischensumme (netto):</td>
|
||||||
|
<td class="summary-value">{formatted_subtotal} €</td>
|
||||||
|
</tr>
|
||||||
|
{tax_breakdown_html}
|
||||||
|
<tr class="grand-total-row">
|
||||||
|
<td class="summary-label">Gesamtbetrag:</td>
|
||||||
|
<td class="summary-value">{formatted_grand_total} €</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notes_html}
|
||||||
|
|
||||||
|
<div class="payment-info">
|
||||||
|
Bitte überweisen Sie den Rechnungsbetrag von <strong>{formatted_grand_total} €</strong> bis zum <strong>{due_date}</strong> auf das unten stehende Bankkonto.<br>
|
||||||
|
Verwendungszweck: <strong>Rechnung {invoice_number}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="footer-grid">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="footer-heading">Unternehmensangaben</div>
|
||||||
|
{seller.get('company_name', '')}<br>
|
||||||
|
Inhaber: {seller.get('owner', '')}<br>
|
||||||
|
{seller.get('street', '')}<br>
|
||||||
|
{seller.get('zip_city', '')}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="footer-heading">Bankverbindung</div>
|
||||||
|
Bank: {seller.get('bank_name', '')}<br>
|
||||||
|
IBAN: {seller.get('iban', '')}<br>
|
||||||
|
BIC: {seller.get('bic', '')}<br>
|
||||||
|
Verwendungszweck: {invoice_number}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="footer-heading">Steuerangaben</div>
|
||||||
|
Steuernummer: {seller.get('tax_number', '')}<br>
|
||||||
|
USt-IdNr.: {seller.get('vat_id', '')}<br>
|
||||||
|
Amtsgericht: {seller.get('court', 'München')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# HTML zu PDF rendern
|
||||||
|
HTML(string=html_content).write_pdf(output_path)
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
def generate_dynamic_invoice(school_name, address, price_val=250.00):
|
||||||
|
"""Erzeugt eine dynamische Rechnungs-PDF für einen Neukunden."""
|
||||||
|
now = datetime.now()
|
||||||
|
inv_num = f"INV-{now.strftime('%Y%m%d')}-{now.strftime('%H%M%S')}"
|
||||||
|
inv_date = now.strftime("%d.%m.%Y")
|
||||||
|
due_date = (now + timedelta(days=14)).strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
customer = {
|
||||||
|
"name": school_name,
|
||||||
|
"street": address.split(',')[0] if ',' in address else address,
|
||||||
|
"zip_city": address.split(',')[1].strip() if ',' in address else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"description": f"Invario Inventarsystem – Jahreslizenz",
|
||||||
|
"details": f"Softwarenutzung für {school_name} (12 Monate)",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit_price": float(price_val),
|
||||||
|
"tax_rate": 19.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
out_path = os.path.join(tempfile.gettempdir(), f"rechnung_{inv_num}.pdf")
|
||||||
|
generate_invoice_pdf(
|
||||||
|
invoice_number=inv_num,
|
||||||
|
invoice_date=inv_date,
|
||||||
|
due_date=due_date,
|
||||||
|
customer=customer,
|
||||||
|
seller=SELLER_INFO,
|
||||||
|
items=items,
|
||||||
|
output_path=out_path,
|
||||||
|
notes="Zahlbar innerhalb von 14 Tagen ohne Abzug."
|
||||||
|
)
|
||||||
|
return out_path
|
||||||
|
|
||||||
def send_accreditation_email(
|
def send_accreditation_email(
|
||||||
recipient: str,
|
recipient: str,
|
||||||
domain: str,
|
domain: str,
|
||||||
@@ -348,7 +816,7 @@ def send_accreditation_email(
|
|||||||
software_name: str = "Invario Inventarsystem",
|
software_name: str = "Invario Inventarsystem",
|
||||||
invoice_path: str = None
|
invoice_path: str = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Generiert den Hauptvertrag temporär und versendet ihn zusammen mit Rechnung, AGB & AVV per E-Mail."""
|
"""Generiert den Hauptvertrag und die Rechnung temporär und versendet sie zusammen mit AGB & AVV per E-Mail."""
|
||||||
|
|
||||||
# 1. Hauptvertrag dynamisch erzeugen
|
# 1. Hauptvertrag dynamisch erzeugen
|
||||||
safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip()
|
safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip()
|
||||||
@@ -364,6 +832,24 @@ def send_accreditation_email(
|
|||||||
output_path=contract_pdf_path
|
output_path=contract_pdf_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 1b. Rechnung dynamisch erzeugen (falls nicht direkt übergeben)
|
||||||
|
generated_invoice_path = None
|
||||||
|
if not invoice_path:
|
||||||
|
# Konvertiert "250,00" zu 250.00 für die Rechnungsfunktion
|
||||||
|
try:
|
||||||
|
price_float = float(price.replace('.', '').replace(',', '.'))
|
||||||
|
except ValueError:
|
||||||
|
price_float = 250.00
|
||||||
|
|
||||||
|
generated_invoice_path = generate_dynamic_invoice(
|
||||||
|
school_name=school_name,
|
||||||
|
address=address,
|
||||||
|
price_val=price_float
|
||||||
|
)
|
||||||
|
invoice_to_attach = generated_invoice_path
|
||||||
|
else:
|
||||||
|
invoice_to_attach = invoice_path
|
||||||
|
|
||||||
# 2. Statischen Ordner aus Flask dynamisch ermitteln (verhindert Pfadfehler im Container)
|
# 2. Statischen Ordner aus Flask dynamisch ermitteln (verhindert Pfadfehler im Container)
|
||||||
if current_app:
|
if current_app:
|
||||||
static_dir = current_app.static_folder
|
static_dir = current_app.static_folder
|
||||||
@@ -386,10 +872,6 @@ def send_accreditation_email(
|
|||||||
agb_pdf_path = get_valid_pdf_path("AGB Invario.pdf")
|
agb_pdf_path = get_valid_pdf_path("AGB Invario.pdf")
|
||||||
avv_pdf_path = get_valid_pdf_path("AVV Invario.pdf")
|
avv_pdf_path = get_valid_pdf_path("AVV Invario.pdf")
|
||||||
|
|
||||||
# Standard-Musterrechnung suchen, falls kein Pfad explizit übergeben wurde
|
|
||||||
if not invoice_path:
|
|
||||||
invoice_path = get_valid_pdf_path("rechnung_INV-20260730-100002-577983.pdf")
|
|
||||||
|
|
||||||
# 3. E-Mail Inhalte
|
# 3. E-Mail Inhalte
|
||||||
subject = "Ihre Zugangsdaten und Unterlagen für Invario"
|
subject = "Ihre Zugangsdaten und Unterlagen für Invario"
|
||||||
|
|
||||||
@@ -409,6 +891,7 @@ def send_accreditation_email(
|
|||||||
<h2 style="color: #2c3e50; margin-top: 0;">Willkommen bei Invario!</h2>
|
<h2 style="color: #2c3e50; margin-top: 0;">Willkommen bei Invario!</h2>
|
||||||
<p style="font-size: 15px; line-height: 1.6;">
|
<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.
|
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>
|
</p>
|
||||||
|
|
||||||
<h3 style="color: #2c3e50;">Ihre Zugangsdaten:</h3>
|
<h3 style="color: #2c3e50;">Ihre Zugangsdaten:</h3>
|
||||||
@@ -430,10 +913,10 @@ def send_accreditation_email(
|
|||||||
if os.path.exists(contract_pdf_path):
|
if os.path.exists(contract_pdf_path):
|
||||||
attachments.append(contract_pdf_path)
|
attachments.append(contract_pdf_path)
|
||||||
|
|
||||||
if invoice_path and os.path.exists(invoice_path):
|
if invoice_to_attach and os.path.exists(invoice_to_attach):
|
||||||
attachments.append(invoice_path)
|
attachments.append(invoice_to_attach)
|
||||||
else:
|
else:
|
||||||
print("[WARNING] Keine Rechnungs-PDF gefunden.")
|
print(f"[WARNING] Rechnung unter {invoice_to_attach} nicht gefunden.")
|
||||||
|
|
||||||
if agb_pdf_path:
|
if agb_pdf_path:
|
||||||
attachments.append(agb_pdf_path)
|
attachments.append(agb_pdf_path)
|
||||||
@@ -445,13 +928,21 @@ def send_accreditation_email(
|
|||||||
else:
|
else:
|
||||||
print(f"[WARNING] AVV unter static/(downloads) nicht gefunden.")
|
print(f"[WARNING] AVV unter static/(downloads) nicht gefunden.")
|
||||||
|
|
||||||
# 5. E-Mail versenden & temporären Vertrag aufräumen
|
# 5. E-Mail versenden & temporäre Dateien aufräumen
|
||||||
try:
|
try:
|
||||||
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
|
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
|
||||||
return success
|
return success
|
||||||
finally:
|
finally:
|
||||||
|
# Hauptvertrag löschen
|
||||||
if os.path.exists(contract_pdf_path):
|
if os.path.exists(contract_pdf_path):
|
||||||
try:
|
try:
|
||||||
os.remove(contract_pdf_path)
|
os.remove(contract_pdf_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"[WARNING] Konnte temporären Vertrag nicht löschen: {e}")
|
print(f"[WARNING] Konnte temporären Vertrag nicht löschen: {e}")
|
||||||
|
|
||||||
|
# Temporär erstellte Rechnung löschen
|
||||||
|
if generated_invoice_path and os.path.exists(generated_invoice_path):
|
||||||
|
try:
|
||||||
|
os.remove(generated_invoice_path)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"[WARNING] Konnte temporäre Rechnung nicht löschen: {e}")
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<h2>2. Registergericht und Registernummer</h2>
|
<h2>2. Registergericht und Registernummer</h2>
|
||||||
<p>
|
<p>
|
||||||
<strong>Registergericht:</strong> Amtsgericht Traunstein<br>
|
<strong>Registergericht:</strong> Amtsgericht Traunstein<br>
|
||||||
<strong>Registernummer:</strong> HRB 123456<br>
|
<strong>Registernummer:</strong> HRB 35441<br>
|
||||||
<strong>USt-ID:</strong> DE 123 456 789
|
<strong>USt-ID:</strong> DE 123 456 789
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user