"""Invoice PDF generation using Jinja2 + WeasyPrint.""" import logging import os from datetime import datetime, timezone from pathlib import Path logger = logging.getLogger(__name__) # PDF output directory (mounted as a Docker volume in production) PDF_DIR = Path(os.getenv("PDF_DIR", "/app/data/invoices")) def generate_invoice_pdf(invoice_id: str, db_session) -> str: """ Render the invoice HTML template and convert to PDF via WeasyPrint. Args: invoice_id: UUID of the Invoice to generate db_session: synchronous SQLAlchemy session (used from Celery tasks) Returns: Absolute path to the generated PDF file. """ from app.models.billing import Invoice, InvoiceLineItem from app.models.school import School from jinja2 import Environment, FileSystemLoader from weasyprint import HTML # Load invoice + line items + school inv = db_session.get(Invoice, invoice_id) if not inv: raise ValueError(f"Invoice {invoice_id} not found") school = db_session.get(School, inv.school_id) line_items = db_session.query(InvoiceLineItem).filter_by(invoice_id=invoice_id).all() # Format dates def _fmt(d) -> str: if not d: return "" if hasattr(d, "strftime"): return d.strftime("%B %d, %Y") return str(d) context = { "invoice": inv, "school": school, "line_items": line_items, "issued_date": _fmt(inv.created_at), "period_start": _fmt(inv.billing_period_start), "period_end": _fmt(inv.billing_period_end), "due_date": _fmt(inv.due_date), "paid_date": _fmt(inv.paid_at), "generated_date": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"), } # Render Jinja2 template template_dir = Path(__file__).parent.parent / "templates" env = Environment(loader=FileSystemLoader(str(template_dir)), autoescape=True) template = env.get_template("invoice.html") html_content = template.render(**context) # Generate PDF PDF_DIR.mkdir(parents=True, exist_ok=True) output_path = PDF_DIR / f"{invoice_id}.pdf" HTML(string=html_content).write_pdf(str(output_path)) logger.info("Generated PDF: %s", output_path) return str(output_path)