Fix: PDF — breiter Header-Balken mit Logo, eine Seite, Hinweis-Prefix, Footer fix
This commit is contained in:
parent
0a466ef6ce
commit
8e36eb0611
1 changed files with 75 additions and 72 deletions
|
|
@ -66,7 +66,7 @@ def _generate_pdf(invoice, items) -> bytes:
|
||||||
|
|
||||||
KLEINUNTERNEHMER = os.getenv("KLEINUNTERNEHMER", "true").lower() == "true"
|
KLEINUNTERNEHMER = os.getenv("KLEINUNTERNEHMER", "true").lower() == "true"
|
||||||
STEUERNUMMER = os.getenv("STEUERNUMMER", "")
|
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")
|
FIRMA = os.getenv("RECHNUNG_GESCHAEFTSNAME", "Ban Yaro")
|
||||||
STRASSE = os.getenv("RECHNUNG_STRASSE", "")
|
STRASSE = os.getenv("RECHNUNG_STRASSE", "")
|
||||||
PLZ_ORT = os.getenv("RECHNUNG_PLZ_ORT", "")
|
PLZ_ORT = os.getenv("RECHNUNG_PLZ_ORT", "")
|
||||||
|
|
@ -76,11 +76,11 @@ def _generate_pdf(invoice, items) -> bytes:
|
||||||
BIC = os.getenv("RECHNUNG_BIC", "")
|
BIC = os.getenv("RECHNUNG_BIC", "")
|
||||||
BANKNAME = os.getenv("RECHNUNG_BANK", "")
|
BANKNAME = os.getenv("RECHNUNG_BANK", "")
|
||||||
|
|
||||||
OR = (230, 126, 34) # Ban Yaro Orange
|
OR = (230, 126, 34)
|
||||||
DK = (30, 30, 30) # Dunkelgrau Text
|
DK = (30, 30, 30)
|
||||||
GY = (130, 130, 130) # Grau
|
GY = (130, 130, 130)
|
||||||
LG = (245, 245, 245) # Hellgrau Hintergrund
|
LG = (245, 245, 245)
|
||||||
WH = (255, 255, 255) # Weiss
|
WH = (255, 255, 255)
|
||||||
|
|
||||||
def eur(v: float) -> str:
|
def eur(v: float) -> str:
|
||||||
s = f"{v:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
s = f"{v:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
|
@ -98,41 +98,50 @@ def _generate_pdf(invoice, items) -> bytes:
|
||||||
except Exception:
|
except Exception:
|
||||||
due_date = ""
|
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 = FPDF()
|
||||||
pdf.add_page()
|
pdf.add_page()
|
||||||
pdf.set_margins(20, 15, 20)
|
pdf.set_margins(20, 0, 20)
|
||||||
pdf.set_auto_page_break(auto=True, margin=28)
|
pdf.set_auto_page_break(auto=True, margin=22)
|
||||||
W = 170 # nutzbareite Breite (210 - 20 - 20)
|
W = 170
|
||||||
|
|
||||||
# ── Orangener Balken oben ─────────────────────────────────────
|
# ── Header-Balken (volle Breite, 16mm) ───────────────────────
|
||||||
pdf.set_fill_color(*OR)
|
pdf.set_fill_color(*OR)
|
||||||
pdf.rect(20, 15, W, 1.5, "F")
|
pdf.rect(0, 0, 210, 16, "F")
|
||||||
|
|
||||||
# ── Firmenname rechts oben ────────────────────────────────────
|
# App-Icon links im Balken
|
||||||
pdf.set_xy(20, 20)
|
if os.path.exists(icon_path):
|
||||||
pdf.set_font("Helvetica", "B", 22)
|
pdf.image(icon_path, x=18, y=1, w=14, h=14)
|
||||||
pdf.set_text_color(*OR)
|
|
||||||
pdf.cell(W, 10, FIRMA, align="R", new_x="LMARGIN", new_y="NEXT")
|
|
||||||
|
|
||||||
# ── Absenderadresse rechts, klein ─────────────────────────────
|
# "Ban Yaro" in Weiss rechts im Balken
|
||||||
pdf.set_font("Helvetica", "", 8.5)
|
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)
|
pdf.set_text_color(*GY)
|
||||||
|
y_addr = 19
|
||||||
for line in filter(None, [INHABER, STRASSE, PLZ_ORT, EMAIL, WEBSITE]):
|
for line in filter(None, [INHABER, STRASSE, PLZ_ORT, EMAIL, WEBSITE]):
|
||||||
pdf.set_x(20)
|
pdf.set_xy(20, y_addr)
|
||||||
pdf.cell(W, 4.5, line, align="R", new_x="LMARGIN", new_y="NEXT")
|
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]))
|
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_font("Helvetica", "", 6.5)
|
||||||
pdf.set_text_color(*GY)
|
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_draw_color(*GY)
|
||||||
pdf.set_line_width(0.15)
|
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) ───────────────────
|
# ── Empfänger links ───────────────────────────────────────────
|
||||||
pdf.set_xy(20, 63)
|
pdf.set_xy(20, 52)
|
||||||
pdf.set_font("Helvetica", "B", 10)
|
pdf.set_font("Helvetica", "B", 10)
|
||||||
pdf.set_text_color(*DK)
|
pdf.set_text_color(*DK)
|
||||||
pdf.cell(85, 5.5, invoice["recipient_name"], new_x="LMARGIN", new_y="NEXT")
|
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.set_text_color(*GY)
|
||||||
pdf.cell(85, 5, invoice["recipient_email"])
|
pdf.cell(85, 5, invoice["recipient_email"])
|
||||||
|
|
||||||
# ── Info-Block rechts (auf Empfänger-Höhe) ────────────────────
|
# ── Info-Block rechts ─────────────────────────────────────────
|
||||||
# x=110, label 35mm + wert 25mm = 60mm → endet bei 170mm ✓
|
|
||||||
info_rows = [
|
info_rows = [
|
||||||
("Rechnungsnummer", invoice["invoice_number"]),
|
("Rechnungsnummer", invoice["invoice_number"]),
|
||||||
("Datum", fdate(invoice.get("created_at", ""))),
|
("Datum", fdate(invoice.get("created_at", ""))),
|
||||||
|
|
@ -157,7 +165,7 @@ def _generate_pdf(invoice, items) -> bytes:
|
||||||
if invoice.get("service_period"):
|
if invoice.get("service_period"):
|
||||||
info_rows.append(("Leistungszeitraum", invoice["service_period"]))
|
info_rows.append(("Leistungszeitraum", invoice["service_period"]))
|
||||||
|
|
||||||
y_info = 63
|
y_info = 52
|
||||||
for lbl, val in info_rows:
|
for lbl, val in info_rows:
|
||||||
pdf.set_xy(110, y_info)
|
pdf.set_xy(110, y_info)
|
||||||
pdf.set_font("Helvetica", "", 8.5)
|
pdf.set_font("Helvetica", "", 8.5)
|
||||||
|
|
@ -169,17 +177,16 @@ def _generate_pdf(invoice, items) -> bytes:
|
||||||
y_info += 6
|
y_info += 6
|
||||||
|
|
||||||
# ── Betreff ───────────────────────────────────────────────────
|
# ── Betreff ───────────────────────────────────────────────────
|
||||||
pdf.set_xy(20, 100)
|
pdf.set_xy(20, 90)
|
||||||
pdf.set_font("Helvetica", "B", 14)
|
pdf.set_font("Helvetica", "B", 13)
|
||||||
pdf.set_text_color(*DK)
|
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_draw_color(*OR)
|
||||||
pdf.set_line_width(0.6)
|
pdf.set_line_width(0.6)
|
||||||
pdf.line(20, pdf.get_y(), 190, pdf.get_y())
|
pdf.line(20, pdf.get_y(), 190, pdf.get_y())
|
||||||
pdf.ln(5)
|
pdf.ln(4)
|
||||||
|
|
||||||
# ── Positionen-Tabelle ────────────────────────────────────────
|
# ── Positionen-Tabelle ────────────────────────────────────────
|
||||||
# Spalten: 90 + 18 + 32 + 30 = 170mm ✓
|
|
||||||
CW = (90, 18, 32, 30)
|
CW = (90, 18, 32, 30)
|
||||||
|
|
||||||
pdf.set_fill_color(*OR)
|
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",
|
pdf.cell(CW[3], 7, eur(item["total"]), border="B", fill=True, align="R",
|
||||||
new_x="LMARGIN", new_y="NEXT")
|
new_x="LMARGIN", new_y="NEXT")
|
||||||
|
|
||||||
pdf.ln(5)
|
pdf.ln(4)
|
||||||
|
|
||||||
# ── Summenblock (rechtsbündig, x=110) ─────────────────────────
|
# ── Summenblock ───────────────────────────────────────────────
|
||||||
# x=110: label 50mm + wert 30mm = 80mm → endet bei 190mm ✓
|
|
||||||
def srow(lbl, val, bold=False, txt_color=None, bg=None):
|
def srow(lbl, val, bold=False, txt_color=None, bg=None):
|
||||||
pdf.set_x(110)
|
pdf.set_x(110)
|
||||||
pdf.set_fill_color(*(bg or WH))
|
pdf.set_fill_color(*(bg or WH))
|
||||||
pdf.set_text_color(*(txt_color or DK))
|
pdf.set_text_color(*(txt_color or DK))
|
||||||
pdf.set_font("Helvetica", "B" if bold else "", 10 if bold else 9)
|
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(50, 6, 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(30, 6, val, align="R", fill=bool(bg), new_x="LMARGIN", new_y="NEXT")
|
||||||
|
|
||||||
srow("Nettobetrag:", eur(invoice["amount_net"]))
|
srow("Nettobetrag:", eur(invoice["amount_net"]))
|
||||||
|
|
||||||
if invoice.get("discount_pct") and invoice["discount_pct"] > 0:
|
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(f"Rabatt ({invoice['discount_pct']:.0f}%):", f"- {eur(invoice['discount_amount'])}", txt_color=OR)
|
||||||
srow("Nach Rabatt:", eur(invoice["amount_after_discount"]))
|
srow("Nach Rabatt:", eur(invoice["amount_after_discount"]))
|
||||||
|
|
||||||
if not KLEINUNTERNEHMER and invoice.get("tax_rate", 0) > 0:
|
if not KLEINUNTERNEHMER and invoice.get("tax_rate", 0) > 0:
|
||||||
srow(f"MwSt. {invoice['tax_rate']:.0f}%:", eur(invoice["tax_amount"]))
|
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.line(110, pdf.get_y(), 190, pdf.get_y())
|
||||||
pdf.ln(1)
|
pdf.ln(1)
|
||||||
srow("Gesamtbetrag:", eur(invoice["amount_gross"]), bold=True, bg=LG)
|
srow("Gesamtbetrag:", eur(invoice["amount_gross"]), bold=True, bg=LG)
|
||||||
pdf.ln(4)
|
pdf.ln(3)
|
||||||
|
|
||||||
# ── Steuerhinweis / Kleinunternehmer ──────────────────────────
|
# ── §19-Hinweis ───────────────────────────────────────────────
|
||||||
if KLEINUNTERNEHMER:
|
if KLEINUNTERNEHMER:
|
||||||
pdf.set_x(20)
|
pdf.set_x(20)
|
||||||
pdf.set_font("Helvetica", "I", 8.5)
|
pdf.set_font("Helvetica", "I", 8.5)
|
||||||
pdf.set_text_color(*GY)
|
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 ──────────────────────────────────────────
|
# ── Zahlungsinfo-Box ──────────────────────────────────────────
|
||||||
if IBAN or due_date:
|
pdf.ln(5)
|
||||||
pdf.ln(6)
|
y_box = pdf.get_y()
|
||||||
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_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_font("Helvetica", "", 9)
|
||||||
pdf.set_text_color(*DK)
|
pdf.cell(45, 5.5, lbl)
|
||||||
pay_rows = []
|
pdf.set_font("Helvetica", "" if lbl == "Verwendungszweck:" else "B", 9)
|
||||||
if due_date: pay_rows.append(("Zahlbar bis:", due_date))
|
pdf.cell(0, 5.5, val, new_x="LMARGIN", new_y="NEXT")
|
||||||
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_fill_color(*OR)
|
||||||
pdf.set_x(24)
|
pdf.rect(20, y_box, 2, pdf.get_y() - y_box + 1, "F")
|
||||||
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")
|
|
||||||
|
|
||||||
# ── Notizen ───────────────────────────────────────────────────
|
# ── Notizen ───────────────────────────────────────────────────
|
||||||
if invoice.get("notes"):
|
if invoice.get("notes"):
|
||||||
pdf.ln(5)
|
pdf.ln(4)
|
||||||
pdf.set_x(20)
|
pdf.set_x(20)
|
||||||
pdf.set_font("Helvetica", "I", 9)
|
pdf.set_font("Helvetica", "I", 9)
|
||||||
pdf.set_text_color(*GY)
|
pdf.set_text_color(*GY)
|
||||||
pdf.multi_cell(W, 5, str(invoice["notes"]))
|
pdf.multi_cell(W, 5, str(invoice["notes"]))
|
||||||
|
|
||||||
# ── Footer mit Pflichtangaben ─────────────────────────────────
|
# ── Footer (fixiert auf Seite 1, kein auto-break) ─────────────
|
||||||
pdf.set_y(-18)
|
pdf.set_auto_page_break(False)
|
||||||
|
pdf.set_y(277)
|
||||||
pdf.set_draw_color(*OR)
|
pdf.set_draw_color(*OR)
|
||||||
pdf.set_line_width(0.4)
|
pdf.set_line_width(0.4)
|
||||||
pdf.line(20, pdf.get_y(), 190, pdf.get_y())
|
pdf.line(20, pdf.get_y(), 190, pdf.get_y())
|
||||||
pdf.ln(2)
|
pdf.ln(1.5)
|
||||||
|
|
||||||
footer_parts = [FIRMA, INHABER]
|
footer_parts = [FIRMA, INHABER]
|
||||||
if STEUERNUMMER:
|
if STEUERNUMMER:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue