Bilder und Farbe gehoeren der Firma, dazu ein Vorschau-Knopf
Logo und ein Fussstreifen sind austauschbar (bilder.py: das gewaehlte Bild wird KOPIERT, ein Pfad auf den Desktop waere morgen tot), die Akzentfarbe kommt aus den Einstellungen - Hex-Feld und Farbwaehler. Firmenname, Anschrift, Bank und Steuernummer bleiben gezeichnet; kein Bild kann eine Pflichtangabe verdecken. Der Vorschau-Knopf zeigt das Blatt, bevor es eine Rechnung wird: kein Nummernverbrauch, nichts im Rechnungsordner, "VORSCHAU - keine Rechnung" quer ueber jeder Seite. Sonst kostet jeder Tippfehler einen Storno. Neue Pruefstaende: pruef_bilder.py, pruef_vorschau.py Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
parent
37b5c60266
commit
142dddc647
7 changed files with 636 additions and 78 deletions
121
pdf_renderer.py
121
pdf_renderer.py
|
|
@ -15,7 +15,6 @@ import json
|
|||
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_RIGHT, TA_LEFT, TA_CENTER
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
|
|
@ -24,6 +23,7 @@ from reportlab.platypus import (
|
|||
Flowable,
|
||||
)
|
||||
|
||||
import bilder
|
||||
import config
|
||||
from berechnung import eur, eur_kurz, runde
|
||||
|
||||
|
|
@ -46,11 +46,49 @@ class _AmtMarke(Flowable):
|
|||
self._merker["seite"] = self.canv.getPageNumber()
|
||||
|
||||
# ---- Farben / Masse ---------------------------------------------------------
|
||||
ACCENT = colors.HexColor("#1f5c3d") # gedecktes Gruen (Camping)
|
||||
# Die Akzentfarbe gehoert der Firma, nicht dem Programm: Ueberschriften, die
|
||||
# Linie unter dem Kopf und der Tabellenkopf tragen sie. Ein Handwerker will kein
|
||||
# Camping-Gruen auf seiner Rechnung. Eingestellt wird EINE Farbe - die helle
|
||||
# Flaeche im Tabellenkopf wird daraus gemischt, damit niemand zwei Farben
|
||||
# aufeinander abstimmen muss.
|
||||
AKZENT_VORGABE = "#1f5c3d" # gedecktes Gruen (Camping)
|
||||
|
||||
ACCENT = colors.HexColor(AKZENT_VORGABE)
|
||||
GREY_BG = colors.HexColor("#eef1ee")
|
||||
LINE = colors.HexColor("#9aa39c")
|
||||
DARK = colors.HexColor("#222222")
|
||||
|
||||
|
||||
def _farbe(wert, vorgabe):
|
||||
"""'#1f5c3d' -> Farbe. Alles Unlesbare faellt auf die Vorgabe zurueck.
|
||||
|
||||
Eine kaputte Farbe in der config.json darf niemals das Schreiben der
|
||||
Rechnung verhindern - dann steht der Gast an der Schranke.
|
||||
"""
|
||||
try:
|
||||
return colors.HexColor(str(wert).strip())
|
||||
except (ValueError, AttributeError):
|
||||
if str(wert).strip():
|
||||
print(f"[pdf] Farbe '{wert}' nicht lesbar - nehme {vorgabe}")
|
||||
return colors.HexColor(vorgabe)
|
||||
|
||||
|
||||
def _design_anwenden(cfg):
|
||||
"""Firmenfarbe uebernehmen, bevor die Stile gebaut werden.
|
||||
|
||||
Die Farben stehen als Modulwerte da, weil 13 Stellen im Layout sie lesen.
|
||||
Sie werden hier EINMAL je Rechnung gesetzt - das Programm rendert nie zwei
|
||||
Rechnungen gleichzeitig.
|
||||
"""
|
||||
global ACCENT, GREY_BG
|
||||
design = (cfg or {}).get("design") or {}
|
||||
ACCENT = _farbe(design.get("akzent", AKZENT_VORGABE), AKZENT_VORGABE)
|
||||
# Helle Flaeche = 8 % Akzent auf Weiss. So passt der Tabellenkopf zu jeder
|
||||
# gewaehlten Farbe, ohne dass jemand einen zweiten Wert pflegen muss.
|
||||
GREY_BG = colors.Color(1 - 0.08 * (1 - ACCENT.red),
|
||||
1 - 0.08 * (1 - ACCENT.green),
|
||||
1 - 0.08 * (1 - ACCENT.blue))
|
||||
|
||||
MARGIN_L = 18 * mm
|
||||
MARGIN_R = 18 * mm
|
||||
MARGIN_TOP = 40 * mm # Platz fuer den Kopf
|
||||
|
|
@ -100,36 +138,46 @@ def _styles():
|
|||
|
||||
|
||||
# ---- Kopf + Fusszeile (auf jeder Seite gezeichnet) --------------------------
|
||||
def _kopf_fuss(canvas, doc, cfg, amt=False):
|
||||
def _vorschau_marke(canvas):
|
||||
"""Schraeg ueber das Blatt: das hier ist KEINE Rechnung.
|
||||
|
||||
Die Vorschau soll zeigen, wie das Blatt aussehen wird - ausgedruckt oder
|
||||
weitergereicht darf sie nie mit der Rechnung verwechselt werden. Sonst haette
|
||||
der Gast ein Blatt in der Hand, das keine Nummer verbraucht hat und in keiner
|
||||
Buchhaltung steht.
|
||||
"""
|
||||
canvas.saveState()
|
||||
canvas.setFont("Helvetica-Bold", 62)
|
||||
canvas.setFillColor(colors.Color(0.55, 0.55, 0.55, alpha=0.22))
|
||||
canvas.translate(PAGE_W / 2, PAGE_H / 2)
|
||||
canvas.rotate(38)
|
||||
canvas.drawCentredString(0, 0, "VORSCHAU")
|
||||
canvas.setFont("Helvetica-Bold", 15)
|
||||
canvas.drawCentredString(0, -22 * mm, "keine Rechnung")
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
def _kopf_fuss(canvas, doc, cfg, amt=False, vorschau=False):
|
||||
"""Kopf und Fuss einer Seite. amt=True -> Blatt fuer das Amt (siehe Fusszeile)."""
|
||||
firma = cfg["firma"]
|
||||
canvas.saveState()
|
||||
if vorschau:
|
||||
_vorschau_marke(canvas)
|
||||
|
||||
# --- Kopf ---
|
||||
logo = (firma.get("logo") or "").strip()
|
||||
# relativen Pfad (z.B. "assets/logo.png") portabel aufloesen:
|
||||
# erst neben Tool/exe, dann im gebuendelten Ressourcen-Ordner (.exe)
|
||||
if logo and not os.path.isabs(logo):
|
||||
for _base in (config.BASE_DIR, config.RESOURCE_DIR):
|
||||
_cand = os.path.join(_base, logo)
|
||||
if os.path.exists(_cand):
|
||||
logo = _cand
|
||||
break
|
||||
# Das Kopfbild liegt oben links, Firmenname und Untertitel stehen rechts
|
||||
# daneben - genau wie bisher. Austauschbar ist nur das BILD; die Texte
|
||||
# kommen aus den Einstellungen und bleiben gezeichnet.
|
||||
img, iw, ih = bilder.laden(firma.get("logo"))
|
||||
line_y = PAGE_H - 25.5 * mm # Hoehe der Trennlinie ohne Bild (wie bisher)
|
||||
logo_ok = False
|
||||
if logo and os.path.exists(logo):
|
||||
try:
|
||||
img = ImageReader(logo)
|
||||
iw, ih = img.getSize()
|
||||
box_w, box_h = 60 * mm, 20 * mm # max. Logo-Box oben links
|
||||
scale = min(box_w / iw, box_h / ih)
|
||||
dw, dh = iw * scale, ih * scale
|
||||
ly = PAGE_H - 9 * mm - dh # Oberkante ~9mm vom Blattrand
|
||||
canvas.drawImage(img, MARGIN_L, ly, width=dw, height=dh,
|
||||
preserveAspectRatio=True, mask="auto")
|
||||
logo_ok = True
|
||||
line_y = ly - 3 * mm
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("[pdf] Logo konnte nicht geladen werden:", e)
|
||||
if img:
|
||||
dw, dh = bilder.einpassen(iw, ih, 60 * mm, 20 * mm) # Kasten oben links
|
||||
ly = PAGE_H - 9 * mm - dh # Oberkante ~9mm vom Blattrand
|
||||
canvas.drawImage(img, MARGIN_L, ly, width=dw, height=dh,
|
||||
preserveAspectRatio=True, mask="auto")
|
||||
logo_ok = True
|
||||
line_y = ly - 3 * mm
|
||||
|
||||
if logo_ok:
|
||||
# Firmenname + Untertitel rechtsbuendig neben dem Logo
|
||||
|
|
@ -172,6 +220,20 @@ def _kopf_fuss(canvas, doc, cfg, amt=False):
|
|||
col_m = MARGIN_L + CONTENT_W * 0.40
|
||||
col_r = PAGE_W - MARGIN_R
|
||||
|
||||
# Das Fusszeilen-Bild steht auf dem AMT-BLATT NICHT. Ein fertiges
|
||||
# Briefpapier-Band traegt fast immer Bank und Steuernummer - und genau die
|
||||
# duerfen dort nicht auftauchen (siehe Trennung oben). Das Amt bekommt
|
||||
# deshalb immer die gezeichnete Fusszeile, auch wenn der Nutzer den Haken
|
||||
# "Firmenangaben stehen im Bild" gesetzt hat.
|
||||
fuss_img, fiw, fih = (None, 0, 0) if amt else bilder.laden(firma.get("fussbild"))
|
||||
if fuss_img:
|
||||
# Schmaler Streifen UNTER den Fusstexten. Die Texte bleiben, wo sie sind -
|
||||
# Firma, Anschrift, Bank und Steuernummer stehen auf jeder Rechnung, egal
|
||||
# welches Bild jemand einstellt.
|
||||
dw, dh = bilder.einpassen(fiw, fih, CONTENT_W, 17 * mm)
|
||||
canvas.drawImage(fuss_img, MARGIN_L + (CONTENT_W - dw) / 2, 7 * mm,
|
||||
width=dw, height=dh, preserveAspectRatio=True, mask="auto")
|
||||
|
||||
rows_l = [] if amt else [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")]
|
||||
rows_m = [firma.get("inhaber", "")] if amt else [firma.get("inhaber", ""),
|
||||
firma.get("steuernummer", "")]
|
||||
|
|
@ -667,7 +729,7 @@ def _kenndaten(rechnung, kopf):
|
|||
return json.dumps(daten, ensure_ascii=False)
|
||||
|
||||
|
||||
def render_rechnung(pfad, cfg, rechnung, kopf):
|
||||
def render_rechnung(pfad, cfg, rechnung, kopf, vorschau=False):
|
||||
"""
|
||||
pfad : Zielpfad der PDF
|
||||
cfg : geladene Konfiguration
|
||||
|
|
@ -679,6 +741,7 @@ def render_rechnung(pfad, cfg, rechnung, kopf):
|
|||
Letzte Seite = Blatt fuers Amt: KEINE Beherbergungssteuer (auch nicht im Gesamt),
|
||||
keine Zwischensumme, keine Bankverbindung, keine Steuernummer.
|
||||
"""
|
||||
_design_anwenden(cfg) # Firmenfarbe VOR den Stilen - sie lesen sie mit
|
||||
st = _styles()
|
||||
|
||||
def bauen(ziel, amt_seite):
|
||||
|
|
@ -702,7 +765,9 @@ def render_rechnung(pfad, cfg, rechnung, kopf):
|
|||
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt
|
||||
|
||||
def seite(c, d):
|
||||
_kopf_fuss(c, d, cfg, amt=(amt_seite is not None and c.getPageNumber() >= amt_seite))
|
||||
_kopf_fuss(c, d, cfg,
|
||||
amt=(amt_seite is not None and c.getPageNumber() >= amt_seite),
|
||||
vorschau=vorschau)
|
||||
|
||||
doc.build(story, onFirstPage=seite, onLaterPages=seite)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue