"""Report engine — renders Jinja2 HTML templates to PDF, DOCX, and HTML.
Uses WeasyPrint for PDF generation and docxtpl for DOCX.
"""
import os
import uuid
import logging
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
from app.config import settings
logger = logging.getLogger(__name__)
class ReportEngine:
"""Template-based report generator supporting PDF, DOCX, and HTML output."""
def __init__(self) -> None:
self.jinja_env = Environment(
loader=FileSystemLoader(settings.REPORT_TEMPLATES_DIR),
autoescape=True,
)
os.makedirs(settings.REPORT_OUTPUT_DIR, exist_ok=True)
def render_html(self, template_name: str, context: dict) -> str:
"""Render a Jinja2 template to an HTML string."""
template = self.jinja_env.get_template(f"{template_name}.html")
context.setdefault("company_name", settings.COMPANY_NAME)
context.setdefault("generated_at", datetime.utcnow().strftime("%B %d, %Y %H:%M UTC"))
return template.render(context)
def generate_pdf(self, template_name: str, context: dict) -> str:
"""Render HTML and convert to PDF with WeasyPrint."""
from weasyprint import HTML, CSS
html_content = self.render_html(template_name, context)
css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css")
output_path = os.path.join(
settings.REPORT_OUTPUT_DIR,
f"{template_name}_{uuid.uuid4().hex[:8]}.pdf",
)
stylesheets = []
if os.path.exists(css_path):
stylesheets.append(CSS(filename=css_path))
HTML(
string=html_content,
base_url=settings.REPORT_TEMPLATES_DIR,
).write_pdf(output_path, stylesheets=stylesheets)
logger.info("PDF generated: %s", output_path)
return output_path
def generate_docx(self, template_name: str, context: dict) -> str:
"""Render a .docx template with docxtpl."""
from docxtpl import DocxTemplate
template_path = os.path.join(
settings.REPORT_TEMPLATES_DIR, f"{template_name}.docx"
)
output_path = os.path.join(
settings.REPORT_OUTPUT_DIR,
f"{template_name}_{uuid.uuid4().hex[:8]}.docx",
)
doc = DocxTemplate(template_path)
context.setdefault("company_name", settings.COMPANY_NAME)
context.setdefault("generated_at", datetime.utcnow().strftime("%B %d, %Y"))
doc.render(context)
doc.save(output_path)
logger.info("DOCX generated: %s", output_path)
return output_path
def generate_html(self, template_name: str, context: dict) -> str:
"""Render and save a standalone HTML report (alias for spec compatibility)."""
return self.generate_html_file(template_name, context)
def generate_html_file(self, template_name: str, context: dict) -> str:
"""Render and save a standalone HTML report."""
html_content = self.render_html(template_name, context)
output_path = os.path.join(
settings.REPORT_OUTPUT_DIR,
f"{template_name}_{uuid.uuid4().hex[:8]}.html",
)
with open(output_path, "w", encoding="utf-8") as f:
f.write(html_content)
logger.info("HTML report generated: %s", output_path)
return output_path
report_engine = ReportEngine()