diff --git a/backend/routes/invoices.py b/backend/routes/invoices.py index a59e231..66a6d63 100644 --- a/backend/routes/invoices.py +++ b/backend/routes/invoices.py @@ -66,7 +66,7 @@ def _generate_pdf(invoice, items) -> bytes: KLEINUNTERNEHMER = os.getenv("KLEINUNTERNEHMER", "true").lower() == "true" STEUERNUMMER = os.getenv("STEUERNUMMER", "") - INHABER = os.getenv("RECHNUNG_INHABER", "René Degelmann") + INHABER = os.getenv("RECHNUNG_INHABER", "Rene Degelmann") FIRMA = os.getenv("RECHNUNG_GESCHAEFTSNAME", "Ban Yaro") STRASSE = os.getenv("RECHNUNG_STRASSE", "") PLZ_ORT = os.getenv("RECHNUNG_PLZ_ORT", "") @@ -76,11 +76,11 @@ def _generate_pdf(invoice, items) -> bytes: BIC = os.getenv("RECHNUNG_BIC", "") BANKNAME = os.getenv("RECHNUNG_BANK", "") - OR = (230, 126, 34) # Ban Yaro Orange - DK = (30, 30, 30) # Dunkelgrau Text - GY = (130, 130, 130) # Grau - LG = (245, 245, 245) # Hellgrau Hintergrund - WH = (255, 255, 255) # Weiss + OR = (230, 126, 34) + DK = (30, 30, 30) + GY = (130, 130, 130) + LG = (245, 245, 245) + WH = (255, 255, 255) def eur(v: float) -> str: s = f"{v:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") @@ -98,41 +98,50 @@ def _generate_pdf(invoice, items) -> bytes: except Exception: due_date = "" + icon_path = os.path.join(os.path.dirname(__file__), "..", "static", "icons", "icon-192.png") + icon_path = os.path.abspath(icon_path) + pdf = FPDF() pdf.add_page() - pdf.set_margins(20, 15, 20) - pdf.set_auto_page_break(auto=True, margin=28) - W = 170 # nutzbareite Breite (210 - 20 - 20) + pdf.set_margins(20, 0, 20) + pdf.set_auto_page_break(auto=True, margin=22) + W = 170 - # ── Orangener Balken oben ───────────────────────────────────── + # ── Header-Balken (volle Breite, 16mm) ─────────────────────── pdf.set_fill_color(*OR) - pdf.rect(20, 15, W, 1.5, "F") + pdf.rect(0, 0, 210, 16, "F") - # ── Firmenname rechts oben ──────────────────────────────────── - pdf.set_xy(20, 20) - pdf.set_font("Helvetica", "B", 22) - pdf.set_text_color(*OR) - pdf.cell(W, 10, FIRMA, align="R", new_x="LMARGIN", new_y="NEXT") + # App-Icon links im Balken + if os.path.exists(icon_path): + pdf.image(icon_path, x=18, y=1, w=14, h=14) - # ── Absenderadresse rechts, klein ───────────────────────────── - pdf.set_font("Helvetica", "", 8.5) + # "Ban Yaro" in Weiss rechts im Balken + pdf.set_xy(20, 1) + pdf.set_font("Helvetica", "B", 20) + pdf.set_text_color(*WH) + pdf.cell(W, 14, FIRMA, align="R") + + # ── Absenderadresse rechts (unterhalb Balken) ───────────────── + pdf.set_font("Helvetica", "", 8) pdf.set_text_color(*GY) + y_addr = 19 for line in filter(None, [INHABER, STRASSE, PLZ_ORT, EMAIL, WEBSITE]): - pdf.set_x(20) - pdf.cell(W, 4.5, line, align="R", new_x="LMARGIN", new_y="NEXT") + pdf.set_xy(20, y_addr) + pdf.cell(W, 4, line, align="R") + y_addr += 4.2 - # ── Absenderzeile über Empfängerfeld (DIN 5008) ─────────────── + # ── Absenderzeile + Trennstrich (DIN 5008) ──────────────────── sender_ref = " · ".join(filter(None, [FIRMA, INHABER, STRASSE, PLZ_ORT])) - pdf.set_xy(20, 56) + pdf.set_xy(20, 46) pdf.set_font("Helvetica", "", 6.5) pdf.set_text_color(*GY) - pdf.cell(85, 4, sender_ref) + pdf.cell(85, 3.5, sender_ref) pdf.set_draw_color(*GY) pdf.set_line_width(0.15) - pdf.line(20, 60.5, 105, 60.5) + pdf.line(20, 50, 105, 50) - # ── Empfänger (links, DIN-5008-Fensterfeld) ─────────────────── - pdf.set_xy(20, 63) + # ── Empfänger links ─────────────────────────────────────────── + pdf.set_xy(20, 52) pdf.set_font("Helvetica", "B", 10) pdf.set_text_color(*DK) pdf.cell(85, 5.5, invoice["recipient_name"], new_x="LMARGIN", new_y="NEXT") @@ -147,8 +156,7 @@ def _generate_pdf(invoice, items) -> bytes: pdf.set_text_color(*GY) pdf.cell(85, 5, invoice["recipient_email"]) - # ── Info-Block rechts (auf Empfänger-Höhe) ──────────────────── - # x=110, label 35mm + wert 25mm = 60mm → endet bei 170mm ✓ + # ── Info-Block rechts ───────────────────────────────────────── info_rows = [ ("Rechnungsnummer", invoice["invoice_number"]), ("Datum", fdate(invoice.get("created_at", ""))), @@ -157,7 +165,7 @@ def _generate_pdf(invoice, items) -> bytes: if invoice.get("service_period"): info_rows.append(("Leistungszeitraum", invoice["service_period"])) - y_info = 63 + y_info = 52 for lbl, val in info_rows: pdf.set_xy(110, y_info) pdf.set_font("Helvetica", "", 8.5) @@ -169,17 +177,16 @@ def _generate_pdf(invoice, items) -> bytes: y_info += 6 # ── Betreff ─────────────────────────────────────────────────── - pdf.set_xy(20, 100) - pdf.set_font("Helvetica", "B", 14) + pdf.set_xy(20, 90) + pdf.set_font("Helvetica", "B", 13) pdf.set_text_color(*DK) - pdf.cell(W, 8, f"Rechnung {invoice['invoice_number']}", new_x="LMARGIN", new_y="NEXT") + pdf.cell(W, 7, f"Rechnung {invoice['invoice_number']}", new_x="LMARGIN", new_y="NEXT") pdf.set_draw_color(*OR) pdf.set_line_width(0.6) pdf.line(20, pdf.get_y(), 190, pdf.get_y()) - pdf.ln(5) + pdf.ln(4) # ── Positionen-Tabelle ──────────────────────────────────────── - # Spalten: 90 + 18 + 32 + 30 = 170mm ✓ CW = (90, 18, 32, 30) pdf.set_fill_color(*OR) @@ -203,24 +210,21 @@ def _generate_pdf(invoice, items) -> bytes: pdf.cell(CW[3], 7, eur(item["total"]), border="B", fill=True, align="R", new_x="LMARGIN", new_y="NEXT") - pdf.ln(5) + pdf.ln(4) - # ── Summenblock (rechtsbündig, x=110) ───────────────────────── - # x=110: label 50mm + wert 30mm = 80mm → endet bei 190mm ✓ + # ── Summenblock ─────────────────────────────────────────────── def srow(lbl, val, bold=False, txt_color=None, bg=None): pdf.set_x(110) pdf.set_fill_color(*(bg or WH)) pdf.set_text_color(*(txt_color or DK)) pdf.set_font("Helvetica", "B" if bold else "", 10 if bold else 9) - pdf.cell(50, 6.5, lbl, align="R", fill=bool(bg)) - pdf.cell(30, 6.5, val, align="R", fill=bool(bg), new_x="LMARGIN", new_y="NEXT") + pdf.cell(50, 6, lbl, align="R", fill=bool(bg)) + pdf.cell(30, 6, val, align="R", fill=bool(bg), new_x="LMARGIN", new_y="NEXT") srow("Nettobetrag:", eur(invoice["amount_net"])) - if invoice.get("discount_pct") and invoice["discount_pct"] > 0: srow(f"Rabatt ({invoice['discount_pct']:.0f}%):", f"- {eur(invoice['discount_amount'])}", txt_color=OR) srow("Nach Rabatt:", eur(invoice["amount_after_discount"])) - if not KLEINUNTERNEHMER and invoice.get("tax_rate", 0) > 0: srow(f"MwSt. {invoice['tax_rate']:.0f}%:", eur(invoice["tax_amount"])) @@ -229,59 +233,58 @@ def _generate_pdf(invoice, items) -> bytes: pdf.line(110, pdf.get_y(), 190, pdf.get_y()) pdf.ln(1) srow("Gesamtbetrag:", eur(invoice["amount_gross"]), bold=True, bg=LG) - pdf.ln(4) + pdf.ln(3) - # ── Steuerhinweis / Kleinunternehmer ────────────────────────── + # ── §19-Hinweis ─────────────────────────────────────────────── if KLEINUNTERNEHMER: pdf.set_x(20) pdf.set_font("Helvetica", "I", 8.5) pdf.set_text_color(*GY) - pdf.multi_cell(W, 5, "Gem. § 19 UStG wird keine Umsatzsteuer berechnet.") + pdf.multi_cell(W, 5, "Hinweis: Gem. § 19 UStG wird keine Umsatzsteuer berechnet.") # ── Zahlungsinfo-Box ────────────────────────────────────────── - if IBAN or due_date: - pdf.ln(6) - y_box = pdf.get_y() + pdf.ln(5) + y_box = pdf.get_y() + pdf.set_x(24) + pdf.set_font("Helvetica", "B", 9) + pdf.set_text_color(*OR) + pdf.cell(W - 4, 6, "Zahlungsinformationen", new_x="LMARGIN", new_y="NEXT") + + pdf.set_font("Helvetica", "", 9) + pdf.set_text_color(*DK) + pay_rows = [] + if due_date: pay_rows.append(("Zahlbar bis:", due_date)) + if IBAN: pay_rows.append(("IBAN:", IBAN)) + if BIC: pay_rows.append(("BIC:", BIC)) + if BANKNAME: pay_rows.append(("Bank:", BANKNAME)) + pay_rows.append( ("Verwendungszweck:", invoice["invoice_number"])) + + for lbl, val in pay_rows: pdf.set_x(24) - pdf.set_font("Helvetica", "B", 9) - pdf.set_text_color(*OR) - pdf.cell(W - 4, 6, "Zahlungsinformationen", new_x="LMARGIN", new_y="NEXT") - pdf.set_font("Helvetica", "", 9) - pdf.set_text_color(*DK) - pay_rows = [] - if due_date: pay_rows.append(("Zahlbar bis:", due_date)) - if IBAN: pay_rows.append(("IBAN:", IBAN)) - if BIC: pay_rows.append(("BIC:", BIC)) - if BANKNAME: pay_rows.append(("Bank:", BANKNAME)) - pay_rows.append( ("Verwendungszweck:", invoice["invoice_number"])) + pdf.cell(45, 5.5, lbl) + pdf.set_font("Helvetica", "" if lbl == "Verwendungszweck:" else "B", 9) + pdf.cell(0, 5.5, val, new_x="LMARGIN", new_y="NEXT") - for lbl, val in pay_rows: - pdf.set_x(24) - pdf.set_font("Helvetica", "", 9) - pdf.cell(45, 5.5, lbl) - pdf.set_font("Helvetica", "" if lbl == "Verwendungszweck:" else "B", 9) - pdf.cell(0, 5.5, val, new_x="LMARGIN", new_y="NEXT") - - # Linker oranger Akzentbalken - pdf.set_fill_color(*OR) - pdf.rect(20, y_box, 2, pdf.get_y() - y_box + 1, "F") + pdf.set_fill_color(*OR) + pdf.rect(20, y_box, 2, pdf.get_y() - y_box + 1, "F") # ── Notizen ─────────────────────────────────────────────────── if invoice.get("notes"): - pdf.ln(5) + pdf.ln(4) pdf.set_x(20) pdf.set_font("Helvetica", "I", 9) pdf.set_text_color(*GY) pdf.multi_cell(W, 5, str(invoice["notes"])) - # ── Footer mit Pflichtangaben ───────────────────────────────── - pdf.set_y(-18) + # ── Footer (fixiert auf Seite 1, kein auto-break) ───────────── + pdf.set_auto_page_break(False) + pdf.set_y(277) pdf.set_draw_color(*OR) pdf.set_line_width(0.4) pdf.line(20, pdf.get_y(), 190, pdf.get_y()) - pdf.ln(2) + pdf.ln(1.5) footer_parts = [FIRMA, INHABER] if STEUERNUMMER: