From 70edcea9abb87570eb689ce312f85db28739743c Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Thu, 27 Aug 2026 22:40:09 +0200 Subject: [PATCH] revert of docker changes and use of the right function --- Website/Dockerfile | 8 +- Website/modules/emailservice/email.py | 519 +++----------------------- Website/requirements.txt | 3 +- 3 files changed, 55 insertions(+), 475 deletions(-) diff --git a/Website/Dockerfile b/Website/Dockerfile index 7a2c5ad..98ad6c4 100644 --- a/Website/Dockerfile +++ b/Website/Dockerfile @@ -5,17 +5,17 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app -# Removed docker.io and sudo to reduce image size and avoid conflicts RUN apt-get update \ && apt-get install -y --no-install-recommends \ git \ iproute2 \ + docker.io \ + sudo \ curl \ openssl \ ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Install specific Docker CLI and Compose plugin versions RUN set -eu; \ arch="$(dpkg --print-architecture)"; \ case "$arch" in \ @@ -36,9 +36,9 @@ RUN set -eu; \ COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -# Copy application sources into the image +# Copy application sources into the image (templates, static, main app, config) COPY . /app EXPOSE 4999 -CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]s \ No newline at end of file +CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"] diff --git a/Website/modules/emailservice/email.py b/Website/modules/emailservice/email.py index 20d58fb..866e145 100644 --- a/Website/modules/emailservice/email.py +++ b/Website/modules/emailservice/email.py @@ -7,7 +7,6 @@ import os import tempfile from fpdf import FPDF from flask import current_app -from weasyprint import HTML SELLER_INFO = { @@ -352,457 +351,41 @@ 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 generate_invoice_fpdf(school_name: str, address: str, price: str, date: str, software_name: str, output_path: str): + """Erstellt eine Rechnung als PDF mittels FPDF2.""" + pdf = FPDF() + pdf.add_page() + + # Header + pdf.set_font("Helvetica", style="B", size=18) + pdf.cell(0, 10, "Rechnung", new_x="LMARGIN", new_y="NEXT", align="L") + pdf.ln(10) + + # Adress- und Datumsblock + pdf.set_font("Helvetica", size=12) + pdf.cell(0, 8, f"Datum: {date}", new_x="LMARGIN", new_y="NEXT", align="R") + pdf.cell(0, 8, school_name, new_x="LMARGIN", new_y="NEXT") + pdf.cell(0, 8, address, new_x="LMARGIN", new_y="NEXT") + pdf.ln(15) + + # Leistung + pdf.set_font("Helvetica", style="B", size=12) + pdf.cell(0, 10, "Leistungsbeschreibung:", new_x="LMARGIN", new_y="NEXT") + pdf.set_font("Helvetica", size=12) + pdf.cell(0, 8, f"Nutzungslizenz / Akkreditierung für {software_name}", new_x="LMARGIN", new_y="NEXT") + pdf.ln(10) + + # Preis + pdf.set_font("Helvetica", style="B", size=12) + pdf.cell(0, 10, f"Zu zahlender Betrag: {price} EUR", new_x="LMARGIN", new_y="NEXT") + pdf.ln(20) + + # Footer + pdf.set_font("Helvetica", size=10) + pdf.cell(0, 8, "Bitte ueberweisen Sie den offenen Betrag innerhalb von 14 Tagen nach Rechnungserhalt.", new_x="LMARGIN", new_y="NEXT") + + # Speichern + pdf.output(output_path) def send_accreditation_email( recipient: str, @@ -816,10 +399,11 @@ def send_accreditation_email( software_name: str = "Invario Inventarsystem", invoice_path: str = None ) -> bool: - """Generiert den Hauptvertrag und die Rechnung temporär und versendet sie zusammen mit AGB & AVV per E-Mail.""" + """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() # 1. Hauptvertrag dynamisch erzeugen - 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) @@ -832,31 +416,30 @@ def send_accreditation_email( output_path=contract_pdf_path ) - # 1b. Rechnung dynamisch erzeugen (falls nicht direkt übergeben) + # 1b. Rechnung dynamisch mit FPDF erzeugen 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_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_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 if current_app: static_dir = current_app.static_folder else: static_dir = os.path.join(os.getcwd(), "static") - # Hilfsfunktion zur flexiblen Pfadfindung ('download' vs 'downloads') def get_valid_pdf_path(filename): possible_paths = [ os.path.join(static_dir, "downloads", filename), @@ -933,14 +516,12 @@ def send_accreditation_email( 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}") - # Temporär erstellte Rechnung löschen if generated_invoice_path and os.path.exists(generated_invoice_path): try: os.remove(generated_invoice_path) diff --git a/Website/requirements.txt b/Website/requirements.txt index 0356a13..170fa45 100644 --- a/Website/requirements.txt +++ b/Website/requirements.txt @@ -4,5 +4,4 @@ bleach>=6.1,<7.0 pymongo>=4.8,<5.0 gunicorn>=22.0,<23.0 requests -fpdf2 -weasyprint \ No newline at end of file +fpdf2 \ No newline at end of file