adddition for the rechunung and the change for the HRB in the Impressum

This commit is contained in:
2026-08-27 22:15:39 +02:00
parent c958ce0e73
commit a5fbe574e7
2 changed files with 502 additions and 11 deletions
+501 -10
View File
@@ -7,8 +7,24 @@ import os
import tempfile
from fpdf import FPDF
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():
smtp = smtplib.SMTP(
"mail.invario-software.de",
@@ -336,6 +352,458 @@ def generate_main_contract_pdf(
pdf.output(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(
recipient: str,
domain: str,
@@ -348,7 +816,7 @@ def send_accreditation_email(
software_name: str = "Invario Inventarsystem",
invoice_path: str = None
) -> 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
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
)
# 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)
if current_app:
static_dir = current_app.static_folder
@@ -386,10 +872,6 @@ def send_accreditation_email(
agb_pdf_path = get_valid_pdf_path("AGB 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
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>
<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>
@@ -430,10 +913,10 @@ def send_accreditation_email(
if os.path.exists(contract_pdf_path):
attachments.append(contract_pdf_path)
if invoice_path and os.path.exists(invoice_path):
attachments.append(invoice_path)
if invoice_to_attach and os.path.exists(invoice_to_attach):
attachments.append(invoice_to_attach)
else:
print("[WARNING] Keine Rechnungs-PDF gefunden.")
print(f"[WARNING] Rechnung unter {invoice_to_attach} nicht gefunden.")
if agb_pdf_path:
attachments.append(agb_pdf_path)
@@ -445,13 +928,21 @@ def send_accreditation_email(
else:
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:
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
return success
finally:
# Hauptvertrag löschen
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}")
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}")
+1 -1
View File
@@ -29,7 +29,7 @@
<h2>2. Registergericht und Registernummer</h2>
<p>
<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
</p>
</section>