Fix 503: make xhtml2pdf import lazy to prevent app crash

If xhtml2pdf fails to install on Flatlogic's server (missing C
libraries), the top-level import crashed the entire WSGI app.
Now it imports lazily inside render_to_pdf() so the app starts
even without xhtml2pdf — only PDF generation degrades gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-02-22 21:07:33 +02:00
parent 71723dcaf4
commit 74cd93fede
2 changed files with 27 additions and 3 deletions

View File

@ -1,10 +1,17 @@
# === PDF GENERATION ===
# Converts a Django HTML template into a PDF file using xhtml2pdf.
# Used for payslip and receipt PDF attachments sent via email.
#
# IMPORTANT: xhtml2pdf is imported LAZILY (inside the function, not at the
# top of the file). This is intentional — if xhtml2pdf fails to install on
# the server (missing C libraries), the rest of the app still works.
# Only PDF generation will fail gracefully.
import logging
from io import BytesIO
from django.template.loader import get_template
from xhtml2pdf import pisa
logger = logging.getLogger(__name__)
def render_to_pdf(template_src, context_dict=None):
@ -21,6 +28,17 @@ def render_to_pdf(template_src, context_dict=None):
if context_dict is None:
context_dict = {}
# --- Lazy import: only load xhtml2pdf when actually generating a PDF ---
# This prevents the entire app from crashing if xhtml2pdf isn't installed.
try:
from xhtml2pdf import pisa
except ImportError:
logger.error(
"xhtml2pdf is not installed — cannot generate PDF. "
"Install it with: pip install xhtml2pdf"
)
return None
# Load and render the HTML template
template = get_template(template_src)
html = template.render(context_dict)

View File

@ -22,7 +22,9 @@ from django.conf import settings
from .models import Worker, Project, WorkLog, Team, PayrollRecord, Loan, PayrollAdjustment
from .forms import AttendanceLogForm, PayrollAdjustmentForm
from .utils import render_to_pdf
# NOTE: render_to_pdf is NOT imported here at the top level.
# It's imported lazily inside process_payment() to avoid crashing the
# entire app if xhtml2pdf is not installed on the server.
# === PAYROLL CONSTANTS ===
@ -812,6 +814,10 @@ def process_payment(request, worker_id):
# EMAIL PAYSLIP (outside the transaction — if email fails, payment is
# still saved. We don't want a network error to roll back a real payment.)
# =========================================================================
# Lazy import — avoids crashing the app if xhtml2pdf isn't installed
from .utils import render_to_pdf
subject = f"Payslip for {worker.name} - {payroll_record.date}"
# Context for both the HTML email body and the PDF attachment
@ -827,7 +833,7 @@ def process_payment(request, worker_id):
html_message = render_to_string('core/email/payslip_email.html', email_context)
plain_message = strip_tags(html_message)
# 2. Render PDF attachment
# 2. Render PDF attachment (returns None if xhtml2pdf is not installed)
pdf_content = render_to_pdf('core/pdf/payslip_pdf.html', email_context)
# 3. Send email with PDF attached