From 68fd9c0e380a37925a963d456534592090f36285 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 15 May 2026 15:50:02 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20En-Dash=20in=20PDF=20durch=20Bindestrich?= =?UTF-8?q?=20ersetzen=20+=20=5Fs()=20Sanitizer=20f=C3=BCr=20alle=20Textei?= =?UTF-8?q?ngaben=20(SW=20by-v982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/routes/admin.py | 6 +++--- backend/routes/invoices.py | 28 ++++++++++++++++++++-------- backend/scheduler.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/admin.js | 2 +- backend/static/sw.js | 2 +- 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/backend/main.py b/backend/main.py index 989217e..9ba2d37 100644 --- a/backend/main.py +++ b/backend/main.py @@ -408,7 +408,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "981" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "982" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 0ff5241..c0ec221 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1339,7 +1339,7 @@ async def _handle_upgrade_invoices(req: dict, new_tier_label: str): price = {"pro": 29.00, "breeder": 49.00}.get(tier, 29.00) today = datetime.now(_TZ).date() end_date = today.replace(year=today.year + 1) - timedelta(days=1) - period = f"{today.strftime('%d.%m.%Y')} – {end_date.strftime('%d.%m.%Y')}" + period = f"{today.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" description = f"{new_tier_label} Jahresabo" billing = conn.execute( @@ -1486,7 +1486,7 @@ async def send_quarterly_report(data: QuarterlyReportBody, user=Depends(require_ count_sent = sum(1 for r in rows if r["status"] == "sent") subject_stb = ( - f"Ban Yaro – Rechnungen Q{data.quarter}/{data.year} " + f"Ban Yaro - Rechnungen Q{data.quarter}/{data.year} " f"({start} bis {end})" ) body_stb = ( @@ -1559,7 +1559,7 @@ async def send_quarterly_report(data: QuarterlyReportBody, user=Depends(require_ "sent_to": data.email, "year": data.year, "quarter": data.quarter, - "period": f"{start} – {end}", + "period": f"{start} - {end}", "count": len(rows), "count_paid": count_paid, "count_sent": count_sent, diff --git a/backend/routes/invoices.py b/backend/routes/invoices.py index 4aceda3..784b5e9 100644 --- a/backend/routes/invoices.py +++ b/backend/routes/invoices.py @@ -82,6 +82,18 @@ def _generate_pdf(invoice, items) -> bytes: LG = (245, 245, 245) WH = (255, 255, 255) + def _s(text) -> str: + """Nicht-Latin1-Zeichen ersetzen bevor sie an fpdf Helvetica übergeben werden.""" + if not text: + return "" + return (str(text) + .replace("–", "-").replace("—", "-") # En/Em-Dash + .replace("‘", "'").replace("’", "'") # Typogr. Anf.zeichen + .replace("“", '"').replace("”", '"') + .replace("…", "...").replace("·", ".") + .replace("€", "EUR") # € falls doch + ) + def eur(v: float) -> str: s = f"{v:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") return f"{s} EUR" @@ -144,17 +156,17 @@ def _generate_pdf(invoice, items) -> bytes: 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") + pdf.cell(85, 5.5, _s(invoice["recipient_name"]), new_x="LMARGIN", new_y="NEXT") pdf.set_font("Helvetica", "", 10) if invoice.get("recipient_address"): for line in str(invoice["recipient_address"]).split("\n"): if line.strip(): pdf.set_x(20) - pdf.cell(85, 5, line.strip(), new_x="LMARGIN", new_y="NEXT") + pdf.cell(85, 5, _s(line.strip()), new_x="LMARGIN", new_y="NEXT") pdf.set_x(20) pdf.set_font("Helvetica", "", 8.5) pdf.set_text_color(*GY) - pdf.cell(85, 5, invoice["recipient_email"]) + pdf.cell(85, 5, _s(invoice["recipient_email"])) # ── Info-Block rechts ───────────────────────────────────────── info_rows = [ @@ -163,7 +175,7 @@ def _generate_pdf(invoice, items) -> bytes: ("Fällig bis", due_date), ] if invoice.get("service_period"): - info_rows.append(("Leistungszeitraum", invoice["service_period"])) + info_rows.append(("Leistungszeitraum", _s(invoice["service_period"]))) y_info = 52 for lbl, val in info_rows: @@ -204,7 +216,7 @@ def _generate_pdf(invoice, items) -> bytes: for i, item in enumerate(items): pdf.set_fill_color(*(LG if i % 2 == 0 else WH)) qty = f"{item['quantity']:.2f}".rstrip("0").rstrip(".") - pdf.cell(CW[0], 7, f" {str(item['description'])[:64]}", border="B", fill=True) + pdf.cell(CW[0], 7, f" {_s(str(item['description']))[:64]}", border="B", fill=True) pdf.cell(CW[1], 7, qty, border="B", fill=True, align="C") pdf.cell(CW[2], 7, eur(item["unit_price"]), border="B", fill=True, align="R") pdf.cell(CW[3], 7, eur(item["total"]), border="B", fill=True, align="R", @@ -240,7 +252,7 @@ def _generate_pdf(invoice, items) -> bytes: pdf.set_x(20) pdf.set_font("Helvetica", "I", 8.5) pdf.set_text_color(*GY) - pdf.multi_cell(W, 5, "Hinweis: Gem. § 19 UStG wird keine Umsatzsteuer berechnet.") + pdf.multi_cell(W, 5, _s("Hinweis: Gem. § 19 UStG wird keine Umsatzsteuer berechnet.") # ── Zahlungsinfo-Box ────────────────────────────────────────── pdf.ln(5) @@ -276,7 +288,7 @@ def _generate_pdf(invoice, items) -> bytes: pdf.set_x(20) pdf.set_font("Helvetica", "I", 9) pdf.set_text_color(*GY) - pdf.multi_cell(W, 5, str(invoice["notes"])) + pdf.multi_cell(W, 5, _s(str(invoice["notes"]))) # ── Footer (fixiert auf Seite 1, kein auto-break) ───────────── pdf.set_auto_page_break(False) @@ -410,7 +422,7 @@ def get_quarterly(year: int, q: int, admin=Depends(require_admin)): labels = {1: "01.01.", 2: "01.04.", 3: "01.07.", 4: "01.10."} ends = {1: "31.03.", 2: "30.06.", 3: "30.09.", 4: "31.12."} - period = f"Q{q} {year} ({labels[q]} – {ends[q]})" + period = f"Q{q} {year} ({labels[q]} - {ends[q]})" with db() as conn: # Alle Rechnungen außer Entwürfe im Quartal (nach Ausstellungsdatum) diff --git a/backend/scheduler.py b/backend/scheduler.py index 19c293e..91e8163 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -226,7 +226,7 @@ async def _create_renewal_invoice_draft(user: dict, expires: date, tier_label: s # Verlängerungszeitraum: Folgetag nach Ablauf bis +1 Jahr start = expires + timedelta(days=1) end = start.replace(year=start.year + 1) - timedelta(days=1) - period = f"{start.strftime('%d.%m.%Y')} – {end.strftime('%d.%m.%Y')}" + period = f"{start.strftime('%d.%m.%Y')} - {end.strftime('%d.%m.%Y')}" with db() as conn: # Nur anlegen wenn noch kein Entwurf/offener Eintrag für diesen User + Zeitraum diff --git a/backend/static/js/app.js b/backend/static/js/app.js index f450de3..b93c518 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '981'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '982'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index a02e1e1..81dc438 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -3646,7 +3646,7 @@ window.Page_admin = (() => { const _now = new Date(); const _end = new Date(_now.getFullYear() + 1, _now.getMonth(), _now.getDate() - 1); const _fmt = d => `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth()+1).padStart(2,'0')}.${d.getFullYear()}`; - const _period = `${_fmt(_now)} – ${_fmt(_end)}`; + const _period = `${_fmt(_now)} - ${_fmt(_end)}`; function _discountNote(reason, count, pct, tierLabel) { const agb = 'Jahresbeitrag gem. AGB. Bei vorzeitiger Kündigung keine anteilige Rückerstattung; Zugang bleibt bis Laufzeitende bestehen.'; diff --git a/backend/static/sw.js b/backend/static/sw.js index b78b525..8136c8e 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v981'; +const CACHE_VERSION = 'by-v982'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache