rt0209/S4HANA_Migration_Intelligence_Agent
0
1"""2ReportGenerator3===============4Produces a professional client-ready DOCX migration assessment report5from one or more AnalysisResult objects.6 7Layout:8 Cover page → Executive Summary → Risk Heatmap table →9 Per-Object Findings (with severity badges) → Effort Matrix →10 Prioritised Remediation Plan → Appendix11"""12import os13import logging14from datetime import date15from pathlib import Path16from typing import List17 18from docx import Document19from docx.shared import Pt, RGBColor, Cm, Inches20from docx.enum.text import WD_ALIGN_PARAGRAPH21from docx.enum.table import WD_TABLE_ALIGNMENT22from docx.oxml.ns import qn23from docx.oxml import OxmlElement24 25from schemas import AnalysisResult, RiskLevel26 27logger = logging.getLogger(__name__)28 29OUTPUT_DIR = Path("reports")30OUTPUT_DIR.mkdir(exist_ok=True)31 32# Severity colour palette (RGB)33SEVERITY_COLORS = {34 RiskLevel.CRITICAL: RGBColor(0xE2, 0x4B, 0x4A),35 RiskLevel.HIGH: RGBColor(0xBA, 0x75, 0x17),36 RiskLevel.MEDIUM: RGBColor(0x18, 0x5F, 0xA5),37 RiskLevel.LOW: RGBColor(0x3B, 0x6D, 0x11),38}39 40SEVERITY_BG = {41 RiskLevel.CRITICAL: RGBColor(0xFC, 0xEB, 0xEB),42 RiskLevel.HIGH: RGBColor(0xFA, 0xEE, 0xDA),43 RiskLevel.MEDIUM: RGBColor(0xE6, 0xF1, 0xFB),44 RiskLevel.LOW: RGBColor(0xEA, 0xF3, 0xDE),45}46 47 48class ReportGenerator:49 50 def generate_docx(51 self,52 results: List[AnalysisResult] | AnalysisResult,53 job_id: str = "report",54 client_name: str = "Client",55 project_name: str = "S/4HANA Migration Assessment",56 ) -> Path:57 if isinstance(results, AnalysisResult):58 results = [results]59 60 doc = Document()61 self._set_margins(doc)62 self._apply_styles(doc)63 64 self._cover_page(doc, client_name, project_name)65 self._executive_summary(doc, results)66 self._risk_heatmap(doc, results)67 68 for result in results:69 self._object_section(doc, result)70 71 self._effort_matrix(doc, results)72 self._remediation_plan(doc, results)73 74 out_path = OUTPUT_DIR / f"migration_report_{job_id[:8]}.docx"75 doc.save(out_path)76 logger.info("Report saved to %s", out_path)77 return out_path78 79 # ── Sections ──────────────────────────────────────────────────────80 81 def _cover_page(self, doc: Document, client: str, project: str):82 doc.add_paragraph()83 doc.add_paragraph()84 title = doc.add_paragraph()85 title.alignment = WD_ALIGN_PARAGRAPH.CENTER86 run = title.add_run(project)87 run.bold = True88 run.font.size = Pt(26)89 run.font.color.rgb = RGBColor(0x18, 0x5F, 0xA5)90 91 sub = doc.add_paragraph()92 sub.alignment = WD_ALIGN_PARAGRAPH.CENTER93 sub.add_run(f"Prepared for {client}").font.size = Pt(14)94 95 date_p = doc.add_paragraph()96 date_p.alignment = WD_ALIGN_PARAGRAPH.CENTER97 date_p.add_run(f"Assessment Date: {date.today().strftime('%d %B %Y')}").font.size = Pt(11)98 99 doc.add_page_break()100 101 def _executive_summary(self, doc: Document, results: List[AnalysisResult]):102 doc.add_heading("1. Executive Summary", level=1)103 104 total_objects = len(results)105 total_effort = sum(r.estimated_effort_days for r in results)106 crit = sum(1 for r in results if r.overall_risk == RiskLevel.CRITICAL)107 high = sum(1 for r in results if r.overall_risk == RiskLevel.HIGH)108 109 p = doc.add_paragraph()110 p.add_run(111 f"This assessment covers {total_objects} custom ABAP object(s) "112 f"identified during the S/4HANA readiness review. "113 f"The total estimated remediation effort is "114 )115 bold = p.add_run(f"{total_effort:.0f} person-days")116 bold.bold = True117 p.add_run(f", with {crit} critical and {high} high-risk findings requiring immediate attention.")118 119 for result in results:120 doc.add_paragraph(f"• {result.object_name}: {result.executive_summary}", style="List Bullet")121 122 def _risk_heatmap(self, doc: Document, results: List[AnalysisResult]):123 doc.add_heading("2. Risk Summary", level=1)124 125 table = doc.add_table(rows=1, cols=5)126 table.style = "Table Grid"127 table.alignment = WD_TABLE_ALIGNMENT.CENTER128 129 headers = ["Object", "Type", "Overall Risk", "Effort (days)", "Readiness Score"]130 for i, h in enumerate(headers):131 cell = table.rows[0].cells[i]132 cell.text = h133 cell.paragraphs[0].runs[0].bold = True134 self._shade_cell(cell, RGBColor(0xE6, 0xF1, 0xFB))135 136 for result in results:137 row = table.add_row()138 row.cells[0].text = result.object_name139 row.cells[1].text = result.object_type140 risk_cell = row.cells[2]141 risk_cell.text = result.overall_risk142 risk_cell.paragraphs[0].runs[0].font.color.rgb = SEVERITY_COLORS.get(result.overall_risk)143 risk_cell.paragraphs[0].runs[0].bold = True144 row.cells[3].text = str(result.estimated_effort_days)145 row.cells[4].text = f"{result.s4_readiness_score}/100"146 147 def _object_section(self, doc: Document, result: AnalysisResult):148 doc.add_heading(f"3. {result.object_name}", level=1)149 150 meta = doc.add_paragraph()151 meta.add_run(f"Type: {result.object_type} | "152 f"Complexity: {result.migration_complexity} | "153 f"Effort: {result.estimated_effort_days} days | "154 f"S/4 Readiness: {result.s4_readiness_score}/100")155 156 doc.add_paragraph(result.executive_summary)157 doc.add_heading("Findings", level=2)158 159 for finding in sorted(result.findings, key=lambda f: list(RiskLevel).index(f.severity)):160 p_title = doc.add_paragraph()161 run = p_title.add_run(f"[{finding.severity}] {finding.title} ({finding.effort_days}d)")162 run.bold = True163 run.font.color.rgb = SEVERITY_COLORS.get(finding.severity)164 165 details = doc.add_paragraph()166 details.paragraph_format.left_indent = Cm(0.5)167 details.add_run("Issue: ").bold = True168 details.add_run(finding.issue + "\n")169 details.add_run("Impact: ").bold = True170 details.add_run(finding.impact + "\n")171 details.add_run("Remediation: ").bold = True172 r = details.add_run(finding.remediation)173 r.font.color.rgb = RGBColor(0x0F, 0x6E, 0x56)174 175 doc.add_paragraph(f"Category: {finding.category} | {finding.line_reference}",176 style="List Bullet")177 178 doc.add_heading("Recommendations", level=2)179 for rec in result.recommendations:180 doc.add_paragraph(rec, style="List Bullet")181 182 doc.add_page_break()183 184 def _effort_matrix(self, doc: Document, results: List[AnalysisResult]):185 doc.add_heading("4. Effort Matrix", level=1)186 187 table = doc.add_table(rows=1, cols=4)188 table.style = "Table Grid"189 for i, h in enumerate(["Object", "Risk Level", "Complexity", "Effort (days)"]):190 cell = table.rows[0].cells[i]191 cell.text = h192 cell.paragraphs[0].runs[0].bold = True193 self._shade_cell(cell, RGBColor(0xE6, 0xF1, 0xFB))194 195 sorted_results = sorted(results, key=lambda r: r.estimated_effort_days, reverse=True)196 for result in sorted_results:197 row = table.add_row()198 row.cells[0].text = result.object_name199 risk_cell = row.cells[1]200 risk_cell.text = result.overall_risk201 risk_cell.paragraphs[0].runs[0].font.color.rgb = SEVERITY_COLORS.get(result.overall_risk)202 row.cells[2].text = result.migration_complexity203 row.cells[3].text = str(result.estimated_effort_days)204 205 total = sum(r.estimated_effort_days for r in results)206 total_row = table.add_row()207 total_row.cells[0].text = "TOTAL"208 total_row.cells[0].paragraphs[0].runs[0].bold = True209 total_row.cells[3].text = f"{total:.0f}"210 total_row.cells[3].paragraphs[0].runs[0].bold = True211 212 def _remediation_plan(self, doc: Document, results: List[AnalysisResult]):213 doc.add_heading("5. Prioritised Remediation Plan", level=1)214 doc.add_paragraph(215 "Address findings in the following order: Critical → High → Medium → Low. "216 "Critical findings must be resolved before any productive migration."217 )218 219 for severity in RiskLevel:220 findings_in_sev = [221 (r.object_name, f)222 for r in results223 for f in r.findings224 if f.severity == severity225 ]226 if not findings_in_sev:227 continue228 229 heading = doc.add_heading(f"{severity} Priority", level=2)230 heading.runs[0].font.color.rgb = SEVERITY_COLORS[severity]231 232 for obj_name, finding in findings_in_sev:233 p = doc.add_paragraph(style="List Bullet")234 p.add_run(f"{obj_name} — {finding.title}: ").bold = True235 p.add_run(finding.remediation)236 237 # ── Helpers ───────────────────────────────────────────────────────238 239 def _set_margins(self, doc: Document):240 for section in doc.sections:241 section.top_margin = Cm(2.5)242 section.bottom_margin = Cm(2.5)243 section.left_margin = Cm(2.5)244 section.right_margin = Cm(2.5)245 246 def _apply_styles(self, doc: Document):247 style = doc.styles["Normal"]248 style.font.name = "Calibri"249 style.font.size = Pt(10.5)250 251 def _shade_cell(self, cell, color: RGBColor):252 tc = cell._tc253 tcPr = tc.get_or_add_tcPr()254 shd = OxmlElement("w:shd")255 hex_color = f"{color[0]:02X}{color[1]:02X}{color[2]:02X}"256 shd.set(qn("w:fill"), hex_color)257 shd.set(qn("w:val"), "clear")258 tcPr.append(shd)259 