revert of docker changes and use of the right function

This commit is contained in:
2026-08-27 22:40:09 +02:00
parent 1e4487edd5
commit 70edcea9ab
3 changed files with 55 additions and 475 deletions
+4 -4
View File
@@ -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
CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]
+50 -469
View File
@@ -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'<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 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)
+1 -2
View File
@@ -4,5 +4,4 @@ bleach>=6.1,<7.0
pymongo>=4.8,<5.0
gunicorn>=22.0,<23.0
requests
fpdf2
weasyprint
fpdf2