CoolFace
Apppublic

rt0209/S4HANA_Migration_Intelligence_Agent

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
report_generator.py259 linesDownload Raw Back to root
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