"""DOCX report generator for compliance analysis results. Uses python-docx (already in requirements.txt). Returns raw bytes so the caller can stream the response without writing to disk. """ from __future__ import annotations from datetime import datetime, timezone from io import BytesIO from docx import Document from docx.shared import Pt, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH from app.domain.compliance.ports import AnalysisRecord _STATUS_LABEL = {"ok": "Compliant", "warn": "Warning", "risk": "Non-Compliant"} _STATUS_COLOR = { "ok": RGBColor(0x22, 0x8B, 0x22), "warn": RGBColor(0xFF, 0x8C, 0x00), "risk": RGBColor(0xDC, 0x14, 0x3C), } def generate_docx(record: AnalysisRecord) -> bytes: """Generate a compliance report DOCX and return its raw bytes. Structure: - Cover: document name, standard, date, risk score - Executive summary (conclusion) - Findings table - Recommended actions - Footer note """ doc = Document() # ── Cover ────────────────────────────────────────────────────────────────── title_para = doc.add_heading("Compliance Analysis Report", level=0) title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER doc.add_paragraph("") meta_table = doc.add_table(rows=4, cols=2) meta_table.style = "Table Grid" labels = ["Document", "Standard", "Date", "Risk Score"] values = [ record.doc_name, record.standard_name, record.created_at.strftime("%Y-%m-%d %H:%M UTC") if record.created_at else "", f"{record.risk_score} / 100", ] for i, (label, value) in enumerate(zip(labels, values)): meta_table.cell(i, 0).text = label meta_table.cell(i, 1).text = value # ── Executive Summary ────────────────────────────────────────────────────── doc.add_heading("Executive Summary", level=1) doc.add_paragraph(record.conclusion) # ── Findings ─────────────────────────────────────────────────────────────── doc.add_heading("Findings", level=1) if record.findings: table = doc.add_table(rows=1, cols=4) table.style = "Table Grid" hdr = table.rows[0].cells for i, h in enumerate(["#", "Status", "Title", "Description / Clause"]): hdr[i].text = h for run in hdr[i].paragraphs[0].runs: run.bold = True for f in record.findings: row = table.add_row().cells row[0].text = str(f.seq + 1) row[1].text = _STATUS_LABEL.get(f.status, f.status) row[2].text = f.title desc = f.description if f.clause_ref: desc += f"\n[{f.clause_ref}]" row[3].text = desc else: doc.add_paragraph("No findings recorded.") # ── Recommended Actions ──────────────────────────────────────────────────── doc.add_heading("Recommended Actions", level=1) for i, action in enumerate(record.actions, start=1): label = action.get("label", "Action") value = action.get("value", "") doc.add_paragraph(f"{i}. {label}: {value}", style="List Number") # ── Footer note ──────────────────────────────────────────────────────────── doc.add_paragraph("") footer = doc.add_paragraph( f"Generated by AI Regulation Analysis System — {datetime.now(timezone.utc).strftime('%Y-%m-%d')}" ) footer.alignment = WD_ALIGN_PARAGRAPH.CENTER for run in footer.runs: run.font.size = Pt(8) run.font.color.rgb = RGBColor(0x88, 0x88, 0x88) buf = BytesIO() doc.save(buf) return buf.getvalue()