""" Server-side PDF generation service for KPI Reports. Uses matplotlib for chart rendering and WeasyPrint for HTML-to-PDF conversion. No browser/JS required — works in Celery tasks and behind firewalls. """ import base64 import io import logging from datetime import datetime from django.template.loader import render_to_string logger = logging.getLogger(__name__) def _render_trend_chart(monthly_data, target, threshold): """ Render the monthly performance trend chart as a base64-encoded PNG. Args: monthly_data: list of 12 KPIReportMonthlyData instances (or None) target: target percentage (float) threshold: threshold percentage (float) Returns: base64-encoded PNG string, or empty string on failure """ try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt except ImportError: logger.warning("matplotlib not available — skipping trend chart") return "" months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] values = [] for m in monthly_data: if m and m.percentage is not None: values.append(float(m.percentage)) else: values.append(None) fig, ax = plt.subplots(figsize=(14, 6.5)) fig.patch.set_facecolor("white") non_none = [(i, v) for i, v in enumerate(values) if v is not None] if non_none: x_indices = [i for i, _ in non_none] y_values = [v for _, v in non_none] ax.plot(range(12), values, color="#005696", linewidth=2, marker="o", markersize=5, zorder=3) ax.scatter(x_indices, y_values, color="#005696", s=40, zorder=4) below = [(i, v) for i, v in non_none if v < threshold] if below: bx = [i for i, _ in below] by = [v for _, v in below] ax.scatter(bx, by, color="#dc2626", s=60, zorder=5, edgecolors="white", linewidths=1) ax.axhline(y=target, color="#16a34a", linestyle="--", linewidth=1.2, alpha=0.7, label=f"Target ({target:.0f}%)") ax.axhline(y=threshold, color="#dc2626", linestyle="--", linewidth=1.2, alpha=0.7, label=f"Threshold ({threshold:.0f}%)") ax.set_ylim(0, 105) ax.set_xticks(range(12)) ax.set_xticklabels(months, fontsize=8) ax.set_ylabel("Percentage (%)", fontsize=9) ax.set_title("Monthly Performance Trend", fontsize=11, fontweight="bold", color="#005696", pad=12) ax.legend(loc="lower right", fontsize=7, framealpha=0.9) ax.grid(True, axis="y", alpha=0.3, linestyle="-") ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format="png", dpi=200, bbox_inches="tight") plt.close() buf.seek(0) return base64.b64encode(buf.getvalue()).decode("utf-8") def _render_source_chart(source_breakdowns): """ Render the complaints-by-source donut chart as a base64-encoded PNG. Args: source_breakdowns: QuerySet of KPIReportSourceBreakdown Returns: base64-encoded PNG string, or empty string on failure """ try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt except ImportError: logger.warning("matplotlib not available — skipping source chart") return "" sources = list(source_breakdowns) if not sources: return "" labels = [s.source_name for s in sources] sizes = [float(s.percentage) for s in sources] counts = [s.complaint_count for s in sources] colors = ["#005696", "#007bbd", "#3b82f6", "#f59e0b", "#10b981", "#8b5cf6", "#ef4444", "#6b7280"] fig, ax = plt.subplots(figsize=(8, 7)) fig.patch.set_facecolor("white") wedges, texts, autotexts = ax.pie( sizes, labels=None, autopct=lambda pct: f"{pct:.0f}%" if pct > 5 else "", colors=colors[: len(sources)], startangle=90, pctdistance=0.80, wedgeprops=dict(width=0.4, edgecolor="white", linewidth=2), ) for autotext in autotexts: autotext.set_color("white") autotext.set_fontsize(8) autotext.set_fontweight("bold") total_count = sum(counts) ax.text(0, 0.05, f"{total_count}", ha="center", va="center", fontsize=20, fontweight="bold", color="#005696") ax.text(0, -0.15, "Total", ha="center", va="center", fontsize=8, color="#64748b") ax.legend( wedges, [f"{l} ({c})" for l, c in zip(labels, counts)], loc="center left", bbox_to_anchor=(1, 0.5), fontsize=8, frameon=False, ) ax.set_title("Complaints by Source", fontsize=11, fontweight="bold", color="#005696", pad=12) plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format="png", dpi=200, bbox_inches="tight") plt.close() buf.seek(0) return base64.b64encode(buf.getvalue()).decode("utf-8") def _get_logo_base64(): """ Load the hospital logo, thumbnail it, and return as a base64 data URI. Returns: base64 data URI string, or empty string on failure """ try: import io from django.conf import settings from PIL import Image as PILImage logo_path = settings.BASE_DIR / "static" / "img" / "HH_P_ICON.png" logo_img = PILImage.open(logo_path) logo_img.thumbnail((500, 500), PILImage.LANCZOS) buf = io.BytesIO() logo_img.save(buf, format="PNG", optimize=True) return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("utf-8") except Exception as e: logger.warning(f"Could not load logo for KPI PDF: {e}") return "" def generate_kpi_report_pdf(report): """ Generate a PDF for a KPI Report using WeasyPrint. Args: report: KPIReport instance with related data loaded Returns: PDF file contents as bytes """ from weasyprint import HTML monthly_data_qs = report.monthly_data.filter(month__gt=0).order_by("month") total_data = report.monthly_data.filter(month=0).first() monthly_data_dict = {m.month: m for m in monthly_data_qs} monthly_data = [monthly_data_dict.get(i) for i in range(1, 13)] source_breakdowns = report.source_breakdowns.all() department_breakdowns = report.department_breakdowns.all() location_breakdowns = report.location_breakdowns.all() target = float(report.target_percentage) if report.target_percentage else 95.0 threshold = float(report.threshold_percentage) if report.threshold_percentage else 90.0 trend_chart_b64 = _render_trend_chart(monthly_data, target, threshold) source_chart_b64 = _render_source_chart(source_breakdowns) ai_analysis = report.ai_analysis or {} context = { "report": report, "monthly_data": monthly_data, "total_data": total_data, "source_breakdowns": source_breakdowns, "department_breakdowns": department_breakdowns, "location_breakdowns": location_breakdowns, "trend_chart": trend_chart_b64, "source_chart": source_chart_b64, "ai_analysis": ai_analysis, "logo_path": _get_logo_base64(), "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), } html_string = render_to_string("analytics/kpi_report_weasyprint.html", context) from django.conf import settings return HTML(string=html_string, base_url=str(settings.BASE_DIR / "static")).write_pdf()