Complete working state of the session. Will be split into two deploy phases (safety scaffolding then feature release) before merging to ai-dev. Includes: - Security fixes (email creds / SECRET_KEY / DEBUG / CSRF) - Backup + restore management commands and browser endpoints - WeasyPrint migration (replaces xhtml2pdf) - New Worker fields + WorkerCertificate + WorkerWarning models - Worker / Team / Project friendly management UIs - Dashboard cert-expiry card + Manage All buttons - Bootstrap tooltips (global init + theme-aware CSS) - Django admin template override (taller M2M pickers) - Money filter for ZAR currency formatting - Resources dropdown nav - Massive CLAUDE.md expansion + deploy plan docs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
# === PDF GENERATION ===
|
|
# Converts a Django HTML template into a PDF file using WeasyPrint.
|
|
# Used for payslip, receipt, and payroll report PDFs (both email and
|
|
# browser download).
|
|
#
|
|
# Why WeasyPrint?
|
|
# ----------------
|
|
# Migrated from xhtml2pdf in Nov 2026. WeasyPrint is a browser-grade
|
|
# HTML-to-PDF renderer — it supports modern CSS features that xhtml2pdf
|
|
# could not: flexbox, grid, @font-face (custom web fonts), box-shadow,
|
|
# border-radius, transforms, and proper CSS cascade handling.
|
|
#
|
|
# WeasyPrint has system dependencies (Pango, Cairo, GDK-PixBuf) that
|
|
# xhtml2pdf did not need. On Flatlogic's Linux environment these are
|
|
# already installed system-wide — no extra setup. On Windows dev
|
|
# machines, we install the GTK3 runtime (via `winget install
|
|
# tschoonj.GTKForWindows`) and then the `_ensure_gtk_on_windows()`
|
|
# helper below tells Python where to find the DLLs.
|
|
#
|
|
# Why the Windows DLL dance?
|
|
# --------------------------
|
|
# Since Python 3.8, `PATH` alone is no longer sufficient to load native
|
|
# DLLs — Python requires explicit `os.add_dll_directory()` calls for
|
|
# security reasons. This helper walks common GTK3 install locations
|
|
# and registers the first one found. On Linux it's a no-op.
|
|
#
|
|
# IMPORTANT: WeasyPrint is imported LAZILY (inside the function, not
|
|
# at the top of the file). This is intentional — if WeasyPrint or its
|
|
# system libraries are missing, the rest of the app still works;
|
|
# only PDF generation fails gracefully and returns None.
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
from django.conf import settings
|
|
from django.template.loader import get_template
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Common install locations for the GTK3 runtime on Windows.
|
|
# Checked in order; first hit wins.
|
|
_WINDOWS_GTK_PATHS = (
|
|
r"C:\Program Files\GTK3-Runtime Win64\bin",
|
|
r"C:\Program Files (x86)\GTK3-Runtime Win64\bin",
|
|
)
|
|
|
|
# Module-level flag so we only add the DLL directory once per process.
|
|
_gtk_registered = False
|
|
|
|
|
|
def _ensure_gtk_on_windows():
|
|
"""Register the GTK3 runtime DLL directory on Windows.
|
|
|
|
WeasyPrint loads DLLs two ways:
|
|
1. `ctypes.util.find_library('gobject-2.0-0')` — reads os.environ['PATH']
|
|
2. `ctypes.CDLL(...)` — uses Windows DLL search, affected by
|
|
`os.add_dll_directory()` since Python 3.8
|
|
|
|
We hit both paths so the library is discoverable regardless of
|
|
which route WeasyPrint chooses. This is a no-op on Linux/macOS
|
|
and on Windows when GTK is already registered.
|
|
"""
|
|
global _gtk_registered
|
|
if _gtk_registered or sys.platform != "win32":
|
|
return
|
|
for path in _WINDOWS_GTK_PATHS:
|
|
if os.path.isdir(path):
|
|
# Prepend to PATH so find_library() locates the DLLs.
|
|
current_path = os.environ.get("PATH", "")
|
|
if path.lower() not in current_path.lower():
|
|
os.environ["PATH"] = path + os.pathsep + current_path
|
|
# Register via add_dll_directory for ctypes.CDLL() loads.
|
|
if hasattr(os, "add_dll_directory"):
|
|
os.add_dll_directory(path)
|
|
_gtk_registered = True
|
|
logger.debug("Registered GTK3 runtime directory: %s", path)
|
|
return
|
|
logger.debug(
|
|
"No GTK3 runtime directory found on Windows in %s", _WINDOWS_GTK_PATHS,
|
|
)
|
|
|
|
|
|
def render_to_pdf(template_src, context_dict=None):
|
|
"""
|
|
Render a Django template to PDF bytes using WeasyPrint.
|
|
|
|
Args:
|
|
template_src: Path to the template
|
|
(e.g. 'core/pdf/payslip_pdf.html')
|
|
context_dict: Template context variables
|
|
|
|
Returns:
|
|
PDF content as bytes, or None if there was an error.
|
|
"""
|
|
if context_dict is None:
|
|
context_dict = {}
|
|
|
|
# On Windows we need to tell Python where the GTK3 DLLs live
|
|
# BEFORE importing weasyprint. Harmless no-op on Linux/macOS.
|
|
_ensure_gtk_on_windows()
|
|
|
|
# --- Lazy import: only load WeasyPrint when actually generating a PDF ---
|
|
# ImportError covers the Python package being missing.
|
|
# OSError covers missing native libs (Pango, Cairo) at runtime.
|
|
try:
|
|
from weasyprint import HTML
|
|
except (ImportError, OSError) as e:
|
|
logger.error(
|
|
"WeasyPrint is not available — cannot generate PDF. "
|
|
"Install with: pip install weasyprint. "
|
|
"On Windows, also install the GTK3 runtime via "
|
|
"`winget install tschoonj.GTKForWindows`. "
|
|
"Underlying error: %s", e,
|
|
)
|
|
return None
|
|
|
|
# Render the Django template to HTML first
|
|
template = get_template(template_src)
|
|
html = template.render(context_dict)
|
|
|
|
# Convert HTML to PDF bytes.
|
|
# base_url lets WeasyPrint resolve relative paths to static files
|
|
# (e.g. fonts in static/fonts/, images in static/img/). We fall back
|
|
# to "." so the call still succeeds when STATIC_ROOT isn't set
|
|
# (e.g. during local dev before collectstatic has run).
|
|
base = getattr(settings, "STATIC_ROOT", None) or "."
|
|
try:
|
|
return HTML(string=html, base_url=base).write_pdf()
|
|
except Exception as e:
|
|
logger.exception("WeasyPrint render failed: %s", e)
|
|
return None
|