diff --git a/Website/modules/emailservice/email.py b/Website/modules/emailservice/email.py index ea302f5..20d58fb 100644 --- a/Website/modules/emailservice/email.py +++ b/Website/modules/emailservice/email.py @@ -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'
{item["details"]}
' if item["details"] else "" + items_html += f""" + + {item['pos']} + +
{item['description']}
+ {details_html} + + {item['quantity']} + {item['unit_price']} € + {item['tax_rate']} + {item['line_total']} € + + """ + + # 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""" + + zzgl. MwSt. ({rate_str}): + {amt_str} € + + """ + + notes_html = f'
Hinweis: {notes}
' if notes else "" + + # HTML & CSS Template + html_content = f""" + + + + Rechnung {invoice_number} + + + + + + + + + +
+
{seller.get('company_name', 'INVARIO')}
+
{seller.get('sub_title', 'Software & Inventarsysteme')}
+
+ {seller.get('company_name', '')}
+ {seller.get('street', '')}
+ {seller.get('zip_city', '')}
+ E-Mail: {seller.get('email', '')}
+ Web: {seller.get('website', '')} +
+ + + + + + +
+
+ {seller.get('company_name', '')} • {seller.get('street', '')} • {seller.get('zip_city', '')} +
+
+ {customer.get('name', '')}
+ {f"z. Hd. {customer['attn']}
" if customer.get('attn') else ""} + {customer.get('street', '')}
+ {customer.get('zip_city', '')} +
+
+
+ + + + + + + + + + + + + + {f'' if customer.get("customer_id") else ''} +
Rechnungs-Nr.:{invoice_number}
Datum:{invoice_date}
Fällig am:{due_date}
Kunden-Nr.:{customer.get("customer_id")}
+
+
+ +
Rechnung {invoice_number}
+
+ Vielen Dank für Ihren Auftrag. Wir stellen Ihnen die nachfolgend aufgeführten Leistungen in Rechnung: +
+ + + + + + + + + + + + + + {items_html} + +
Pos.BeschreibungMengeEinzelpreisMwSt.Gesamt
+ +
+ + + + + + {tax_breakdown_html} + + + + +
Zwischensumme (netto):{formatted_subtotal} €
Gesamtbetrag:{formatted_grand_total} €
+
+ + {notes_html} + +
+ Bitte überweisen Sie den Rechnungsbetrag von {formatted_grand_total} € bis zum {due_date} auf das unten stehende Bankkonto.
+ Verwendungszweck: Rechnung {invoice_number} +
+ + + + + + + + + + + +""" + + # 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(

Willkommen bei Invario!

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.

Ihre Zugangsdaten:

@@ -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}") \ No newline at end of file + 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}") \ No newline at end of file diff --git a/Website/templates/impressum.html b/Website/templates/impressum.html index 4783125..bf10d66 100644 --- a/Website/templates/impressum.html +++ b/Website/templates/impressum.html @@ -29,7 +29,7 @@

2. Registergericht und Registernummer

Registergericht: Amtsgericht Traunstein
- Registernummer: HRB 123456
+ Registernummer: HRB 35441
USt-ID: DE 123 456 789