#!/usr/bin/env python3 """ builders/build_excel.py -- Enhanced Excel workbook builder. Reads the skill's original builder and adds DuPont, SWOT, and Indian unit support. """ import json, sys, datetime from pathlib import Path try: from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter from openpyxl.chart import BarChart, LineChart, Reference except ImportError: print("ERROR: pip install openpyxl", file=sys.stderr); sys.exit(1) # -- Style constants ----------------------------------------------------------- HDR_FILL = PatternFill("solid", fgColor="1F4E78") HDR_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFF") SEC_FILL = PatternFill("solid", fgColor="D9E1F2") SEC_FONT = Font(name="Calibri", size=11, bold=True, color="1F4E78") LBL_FONT = Font(name="Calibri", size=11) TTL_FONT = Font(name="Calibri", size=16, bold=True, color="1F4E78") DAT_FONT = Font(name="Calibri", size=11) GRN_FILL = PatternFill("solid", fgColor="C6EFCE") RED_FILL = PatternFill("solid", fgColor="FFC7CE") YLW_FILL = PatternFill("solid", fgColor="FFEB9C") THIN = Border( left=Side(style="thin", color="BFBFBF"), right=Side(style="thin", color="BFBFBF"), top=Side(style="thin", color="BFBFBF"), bottom=Side(style="thin", color="BFBFBF"), ) CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True) RIGHT = Alignment(horizontal="right", vertical="center") FMT_MONEY = '#,##0;(#,##0);"--"' FMT_DEC = '#,##0.00;(#,##0.00);"--"' FMT_PCT = '0.0%;(0.0%);"--"' FMT_RATIO = '0.00;(0.00);"--"' FMT_DAYS = '0.0;"--"' # -- Helpers ------------------------------------------------------------------- def _safe(arr, i, default=None): try: return arr[i] if arr and arr[i] is not None else default except: return default def _safe_div(num, den, default=None): try: if num is None or den is None: return default if den == 0: return default return num / den except: return default def _hdr(ws, row, cols, start=1): for i, h in enumerate(cols): c = ws.cell(row=row, column=start+i, value=h) c.font, c.fill, c.alignment, c.border = HDR_FONT, HDR_FILL, CENTER, THIN def _sec(ws, row, label, ncols, start=1): ws.cell(row=row, column=start, value=label).font = SEC_FONT ws.cell(row=row, column=start).fill = SEC_FILL ws.cell(row=row, column=start).alignment = LEFT for col in range(start+1, start+ncols): ws.cell(row=row, column=col).fill = SEC_FILL def _data_row(ws, row, label, values, fmt=FMT_MONEY, start=1): c = ws.cell(row=row, column=start, value=label) c.font, c.alignment, c.border = LBL_FONT, LEFT, THIN for i, v in enumerate(values): cell = ws.cell(row=row, column=start+1+i, value=v if v is not None else "--") cell.font, cell.alignment, cell.border = DAT_FONT, RIGHT, THIN if v is not None and isinstance(v, (int, float)): cell.number_format = fmt def _title(ws, title, subtitle=None): ws.cell(row=1, column=1, value=title).font = TTL_FONT if subtitle: ws.cell(row=2, column=1, value=subtitle).font = Font(name="Calibri", size=11, italic=True, color="595959") def _widths(ws, widths): for i, w in enumerate(widths): ws.column_dimensions[get_column_letter(i+1)].width = w def _freeze(ws, hdr_row): ws.freeze_panes = ws.cell(row=hdr_row+1, column=2) def _last(arr): if not arr: return None for v in reversed(arr): if v is not None: return v return None # -- Statement section helper -------------------------------------------------- def _stmt_section(ws, row, title, items, data_dict, years, fmt=FMT_MONEY): _sec(ws, row, title, 1+len(years)) row += 1 for label, key in items: vals = data_dict.get(key, [None]*len(years)) vals = (vals + [None]*len(years))[:len(years)] _data_row(ws, row, label, vals, fmt) row += 1 return row # -- Sheet builders ------------------------------------------------------------ def _summary_sheet(wb, data): ws = wb.create_sheet("Summary") co = data.get("company", {}) units_label = data.get("units_label", data.get("units", "Crores")) currency = data.get("currency", co.get("currency", "INR")) _title(ws, f"{co.get('name','')} ({co.get('ticker','')}) -- Financial Analysis", f"Report date: {co.get('report_date', datetime.date.today().isoformat())}") r = 4 for label, val in [ ("Company", co.get("name","")), ("Ticker", co.get("ticker","")), ("Exchange", co.get("exchange","")), ("Industry", co.get("industry","")), ("Sector", co.get("sector","")), ("Currency", currency), ("Reporting Units", units_label), ("Fiscal Year End", co.get("fiscal_year_end","")), ("Analysis Period", f"{len(data.get('years',[]))} years ({', '.join(data.get('years',[]))})"), ("Share Price", co.get("share_price")), ("Market Cap", co.get("market_cap")), ("Data Source", data.get("source", "Yahoo Finance (yahooquery)")), ("Models Used", ", ".join(data.get("models_used", []) or ["None"])), ("Warnings", " | ".join(data.get("warnings", []))), ]: ws.cell(row=r, column=1, value=label).font = LBL_FONT ws.cell(row=r, column=2, value=val).font = DAT_FONT r += 1 _widths(ws, [28, 50]) def _income_sheet(wb, data): ws = wb.create_sheet("Income Statement") co, years = data.get("company",{}), data.get("years",[]) units_label = data.get("units_label", data.get("units", "Crores")) currency = data.get("currency", co.get("currency", "INR")) _title(ws, f"Income Statement -- {co.get('ticker','')}", f"All figures in {units_label}") _hdr(ws, 4, [f"({units_label})"] + years) inc = data.get("income_statement", {}) r = _stmt_section(ws, 5, "Revenue & Profitability", [ ("Revenue", "revenue"), ("Cost of Revenue", "cost_of_revenue"), ("Gross Profit", "gross_profit"), ("Operating Expenses", "operating_expenses"), ("Operating Income (EBIT)","operating_income"), ("D&A", "depreciation_amort"), ("EBITDA", "ebitda"), ("Interest Expense", "interest_expense"), ("Pretax Income", "pretax_income"), ("Tax Expense", "tax_expense"), ("Net Income", "net_income"), ], inc, years) r += 1 _sec(ws, r, "Per-Share Data", 1+len(years)); r += 1 _data_row(ws, r, "Diluted EPS", inc.get("diluted_eps",[None]*len(years))[:len(years)], FMT_DEC); r += 1 _data_row(ws, r, "Diluted Shares (M)", inc.get("diluted_shares",[None]*len(years))[:len(years)], FMT_MONEY) # Add revenue bar chart if len(years) >= 2: chart = BarChart() chart.title = f"Revenue Trend ({units_label})" chart.style = 10 chart.y_axis.title = units_label rev_row = 6 data_ref = Reference(ws, min_col=2, max_col=1+len(years), min_row=rev_row, max_row=rev_row) cats_ref = Reference(ws, min_col=2, max_col=1+len(years), min_row=4, max_row=4) chart.add_data(data_ref) chart.set_categories(cats_ref) chart.shape = 4 ws.add_chart(chart, f"A{r+3}") _widths(ws, [34]+[16]*len(years)); _freeze(ws, 4) def _balance_sheet(wb, data): ws = wb.create_sheet("Balance Sheet") co, years = data.get("company",{}), data.get("years",[]) units_label = data.get("units_label", data.get("units", "Crores")) _title(ws, f"Balance Sheet -- {co.get('ticker','')}", f"As of fiscal year-end. {units_label}") _hdr(ws, 4, [f"({units_label})"] + years) bs = data.get("balance_sheet", {}) r = 5 for sec_title, items in [ ("Current Assets", [ ("Cash & Equivalents","cash_and_equivalents"),("Short-term Investments","short_term_investments"), ("Receivables","receivables"),("Inventory","inventory"),("Other Current Assets","other_current_assets"), ("Total Current Assets","total_current_assets"), ]), ("Non-Current Assets", [ ("PP&E (Net)","ppe_net"),("Goodwill","goodwill"),("Intangibles","intangibles"), ("Other LT Assets","other_long_term_assets"),("Total Assets","total_assets"), ]), ("Current Liabilities", [ ("Accounts Payable","accounts_payable"),("Short-term Debt","short_term_debt"), ("Other Current Liabilities","other_current_liabilities"), ("Total Current Liabilities","total_current_liabilities"), ]), ("Non-Current Liabilities & Equity", [ ("Long-term Debt","long_term_debt"),("Other LT Liabilities","other_long_term_liabilities"), ("Total Liabilities","total_liabilities"),("Total Equity","total_equity"), ("Total Liabilities + Equity","total_liab_and_equity"), ]), ]: r = _stmt_section(ws, r, sec_title, items, bs, years) _widths(ws, [36]+[16]*len(years)); _freeze(ws, 4) def _cashflow_sheet(wb, data): ws = wb.create_sheet("Cash Flow") co, years = data.get("company",{}), data.get("years",[]) units_label = data.get("units_label", data.get("units", "Crores")) _title(ws, f"Cash Flow Statement -- {co.get('ticker','')}", f"All figures in {units_label}") _hdr(ws, 4, [f"({units_label})"] + years) cf = data.get("cash_flow", {}) r = 5 for sec_title, items in [ ("Operating Activities", [ ("Net Income","net_income"),("D&A","depreciation_amort"),("Stock-Based Comp","stock_based_comp"), ("Working Capital Changes","working_capital_changes"),("Cash from Operations","cash_from_operations"), ]), ("Investing Activities", [ ("Capital Expenditures","capex"),("Acquisitions","acquisitions"),("Cash from Investing","cash_from_investing"), ]), ("Financing Activities", [ ("Debt Issuance/(Repayment)","debt_issuance_repayment"),("Buybacks","stock_buybacks"), ("Dividends Paid","dividends_paid"),("Cash from Financing","cash_from_financing"), ]), ("Summary", [ ("Net Change in Cash","net_change_in_cash"),("Free Cash Flow","free_cash_flow"), ]), ]: r = _stmt_section(ws, r, sec_title, items, cf, years) _widths(ws, [36]+[16]*len(years)); _freeze(ws, 4) RATIO_SECTIONS = [ ("Profitability", [ ("gross_margin","Gross Margin",FMT_PCT),("operating_margin","Operating Margin",FMT_PCT), ("ebitda_margin","EBITDA Margin",FMT_PCT),("net_margin","Net Margin",FMT_PCT), ("roe","ROE",FMT_PCT),("roa","ROA",FMT_PCT),("roic","ROIC",FMT_PCT), ]), ("Liquidity", [ ("current_ratio","Current Ratio",FMT_RATIO),("quick_ratio","Quick Ratio",FMT_RATIO), ("cash_ratio","Cash Ratio",FMT_RATIO), ]), ("Solvency / Leverage", [ ("debt_to_equity","Debt-to-Equity",FMT_RATIO),("debt_to_assets","Debt-to-Assets",FMT_RATIO), ("net_debt_to_ebitda","Net Debt/EBITDA",FMT_RATIO),("interest_coverage","Interest Coverage",FMT_RATIO), ]), ("Efficiency", [ ("asset_turnover","Asset Turnover",FMT_RATIO),("inventory_turnover","Inventory Turnover",FMT_RATIO), ("days_inventory","Days Inventory (DIO)",FMT_DAYS),("days_sales_outstanding","DSO",FMT_DAYS), ("days_payables","DPO",FMT_DAYS),("cash_conversion_cycle","Cash Conversion Cycle",FMT_DAYS), ]), ("Cash Flow Quality", [ ("ocf_to_net_income","OCF/Net Income",FMT_RATIO),("fcf_margin","FCF Margin",FMT_PCT), ("fcf_conversion","FCF Conversion",FMT_RATIO),("capex_intensity","Capex/Revenue",FMT_PCT), ]), ] def _ratios_sheet(wb, data): ws = wb.create_sheet("Ratios") years = data.get("years", []) _title(ws, f"Financial Ratios -- {data.get('company',{}).get('ticker','')}", "5-year trend") _hdr(ws, 4, ["Ratio"] + years) ratios = data.get("ratios", {}) r = 5 for sec_name, items in RATIO_SECTIONS: _sec(ws, r, sec_name, 1+len(years)); r += 1 for key, label, fmt in items: vals = ratios.get(key, [None]*len(years)) vals = (vals + [None]*len(years))[:len(years)] _data_row(ws, r, label, vals, fmt); r += 1 _widths(ws, [40]+[14]*len(years)); _freeze(ws, 4) def _dupont_sheet(wb, data): ws = wb.create_sheet("DuPont Analysis") years = data.get("years", []) _title(ws, "DuPont Decomposition", "ROE = Net Margin × Asset Turnover × Equity Multiplier") _hdr(ws, 4, ["Component"] + years) dp = data.get("dupont", {}) r = 5 for label, key, fmt in [ ("Net Margin", "net_margin", FMT_PCT), ("× Asset Turnover", "asset_turnover", FMT_RATIO), ("× Equity Multiplier","equity_multiplier",FMT_RATIO), ("= ROE (DuPont)", "roe_computed", FMT_PCT), ("ROE (Reported)", "roe_reported", FMT_PCT), ]: vals = dp.get(key, [None]*len(years)) vals = (vals + [None]*len(years))[:len(years)] _data_row(ws, r, label, vals, fmt) r += 1 _widths(ws, [28]+[14]*len(years)) def _growth_sheet(wb, data): ws = wb.create_sheet("Growth Metrics") years = data.get("years", []) yoy_hdrs = [f"{years[i]} vs {years[i-1]}" for i in range(1, len(years))] if len(years) > 1 else [] _title(ws, "Growth Metrics", "YoY growth and CAGRs") _hdr(ws, 4, ["Metric"] + yoy_hdrs + ["CAGR"]) g = data.get("growth_metrics", {}) r = 5 for label, yoy_key, cagr_key in [ ("Revenue", "revenue_yoy", "revenue_cagr_5y"), ("Operating Income","operating_income_yoy","operating_income_cagr_5y"), ("Net Income", "net_income_yoy", "net_income_cagr_5y"), ("Diluted EPS", "eps_yoy", "eps_cagr_5y"), ("Free Cash Flow", "fcf_yoy", "fcf_cagr_5y"), ]: yoy = g.get(yoy_key, [])[1:] # skip first None cagr = g.get(cagr_key) vals = (yoy + [None]*len(yoy_hdrs))[:len(yoy_hdrs)] + [cagr] _data_row(ws, r, label, vals, FMT_PCT); r += 1 _widths(ws, [28]+[16]*(len(yoy_hdrs)+1)) def _peers_sheet(wb, data): ws = wb.create_sheet("Peer Comparison") co = data.get("company", {}) _title(ws, f"Peer Comparison -- {co.get('ticker','')}", "Latest fiscal year") cols = ["Company","Ticker","Revenue","Gross Margin","Op Margin","Net Margin", "ROE","ROIC","D/E","Current Ratio","FCF Margin","P/E","EV/EBITDA","Rationale"] _hdr(ws, 4, cols) r = 5 years = data.get("years", []) rat = data.get("ratios", {}); inc = data.get("income_statement", {}) val = data.get("valuation_snapshot", {}) # Target row target = [ co.get("name",""), co.get("ticker",""), _last(inc.get("revenue",[])), _last(rat.get("gross_margin",[])), _last(rat.get("operating_margin",[])), _last(rat.get("net_margin",[])), _last(rat.get("roe",[])), _last(rat.get("roic",[])), _last(rat.get("debt_to_equity",[])), _last(rat.get("current_ratio",[])), _last(rat.get("fcf_margin",[])), val.get("pe_ttm"), val.get("ev_ebitda"), "Target company", ] fmts = ["","","money","pct","pct","pct","pct","pct","ratio","ratio","pct","ratio","ratio",""] _write_peer_row(ws, r, target, fmts, is_target=True); r += 1 for p in data.get("peers", []): peer_row = [ p.get("name",""), p.get("ticker",""), p.get("revenue"), p.get("gross_margin"), p.get("operating_margin"), p.get("net_margin"), p.get("roe"), p.get("roic"), p.get("debt_to_equity"), p.get("current_ratio"), p.get("fcf_margin"), p.get("pe_ttm"), p.get("ev_ebitda"), p.get("rationale",""), ] _write_peer_row(ws, r, peer_row, fmts); r += 1 _widths(ws, [24,10,16,12,12,12,10,10,10,12,12,10,12,50]) def _write_peer_row(ws, row, values, fmts, is_target=False): fmt_map = {"money":FMT_MONEY,"pct":FMT_PCT,"ratio":FMT_RATIO} fill = PatternFill("solid", fgColor="FFF2CC") if is_target else None for i, v in enumerate(values): c = ws.cell(row=row, column=i+1, value=v if v is not None else "--") c.border = THIN c.font = Font(name="Calibri", size=11, bold=is_target) if fill: c.fill = fill fmt = fmts[i] if i < len(fmts) else "" if fmt in fmt_map and isinstance(v, (int, float)): c.number_format = fmt_map[fmt] c.alignment = RIGHT else: c.alignment = LEFT def _swot_sheet(wb, data): ws = wb.create_sheet("SWOT Analysis") _title(ws, "SWOT Analysis", "Strengths, Weaknesses, Opportunities, Threats") swot = data.get("swot", {}) r = 4 colors = {"Strengths":"C6EFCE","Weaknesses":"FFC7CE","Opportunities":"DDEBF7","Threats":"FFEB9C"} for category, key in [("Strengths","strengths"),("Weaknesses","weaknesses"), ("Opportunities","opportunities"),("Threats","threats")]: fill = PatternFill("solid", fgColor=colors[category]) hdr_cell = ws.cell(row=r, column=1, value=category) hdr_cell.font = Font(name="Calibri", size=12, bold=True) hdr_cell.fill = fill r += 1 for item in swot.get(key, []): c = ws.cell(row=r, column=1, value=f"• {item}") c.font = Font(name="Calibri", size=11) c.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) ws.row_dimensions[r].height = 32 r += 1 r += 1 _widths(ws, [90]) def _dcf_sheet(wb, data): ws = wb.create_sheet("DCF Valuation Model") _title(ws, "Interactive Discounted Cash Flow (DCF)", "Change the yellow highlighted cells to update assumptions") currency = data.get("currency", "USD") units = data.get("company", {}).get("units", "Millions") # 1. Historical data rev_hist = data.get("income_statement", {}).get("revenue", []) fcf_hist = data.get("cash_flow", {}).get("free_cash_flow", []) last_rev = _last(rev_hist) or 0 last_fcf = _last(fcf_hist) or 0 # Default assumptions default_wacc = 0.09 default_tg = 0.025 # Historical FCF Margin & Growth rev_cagr = data.get("growth_metrics", {}).get("revenue_cagr_5y") if rev_cagr is None: rev_cagr = 0.05 rev_cagr = max(0.02, min(0.15, rev_cagr)) # Cap between 2% and 15% fcf_margins = [_safe_div(f, r) for f, r in zip(fcf_hist, rev_hist) if f is not None and r is not None and r != 0] avg_fcf_margin = sum(fcf_margins[-3:]) / len(fcf_margins[-3:]) if len(fcf_margins) > 0 else 0.10 shares_out = _last(data.get("income_statement", {}).get("diluted_shares", [])) or 1 if shares_out <= 0: shares_out = 1 # Prevent division by zero net_debt = _last(data.get("net_debt", [])) or 0 # --- Assumptions Block --- r = 4 _sec(ws, r, "1. Model Assumptions", 3) ws.cell(row=r, column=4, value="Yellow cells are editable").font = Font(italic=True, color="595959") r += 1 def add_assumption(row_idx, label, default_val, is_pct=True): ws.cell(row=row_idx, column=1, value=label).font = LBL_FONT c = ws.cell(row=row_idx, column=2, value=default_val) c.fill = YLW_FILL c.font = Font(name="Calibri", size=11, bold=True, color="1F4E78") c.border = THIN c.alignment = RIGHT c.number_format = FMT_PCT if is_pct else FMT_DEC return c.coordinate wacc_cell = add_assumption(r, "Weighted Average Cost of Capital (WACC)", default_wacc); r += 1 tg_cell = add_assumption(r, "Terminal Growth Rate", default_tg); r += 1 rev_gr_cell = add_assumption(r, "Projected Revenue Growth (Years 1-5)", rev_cagr); r += 1 fcf_m_cell = add_assumption(r, "Projected FCF Margin", avg_fcf_margin); r += 1 r += 1 # --- Projections Block --- _sec(ws, r, "2. Free Cash Flow Projections", 7) r += 1 _hdr(ws, r, ["Metric", "Year 0 (Current)", "Year 1", "Year 2", "Year 3", "Year 4", "Year 5"]) r += 1 # Revenue row ws.cell(row=r, column=1, value=f"Revenue ({currency} {units})").font = LBL_FONT ws.cell(row=r, column=2, value=last_rev).number_format = FMT_MONEY ws.cell(row=r, column=2).font = DAT_FONT rev_cells = [] for i in range(1, 6): c = ws.cell(row=r, column=2+i, value=f"={get_column_letter(2+i-1)}{r}*(1+{rev_gr_cell})") c.number_format = FMT_MONEY; c.font = DAT_FONT rev_cells.append(c.coordinate) r += 1 # FCF row ws.cell(row=r, column=1, value=f"Free Cash Flow ({currency} {units})").font = LBL_FONT ws.cell(row=r, column=2, value=last_fcf).number_format = FMT_MONEY ws.cell(row=r, column=2).font = DAT_FONT fcf_cells = [] for i in range(1, 6): c = ws.cell(row=r, column=2+i, value=f"={rev_cells[i-1]}*{fcf_m_cell}") c.number_format = FMT_MONEY; c.font = DAT_FONT fcf_cells.append(c.coordinate) r += 1 # Discount Factor ws.cell(row=r, column=1, value="Discount Factor").font = LBL_FONT ws.cell(row=r, column=2, value=1.0).number_format = FMT_DEC ws.cell(row=r, column=2).font = Font(italic=True, color="595959") df_cells = [] for i in range(1, 6): c = ws.cell(row=r, column=2+i, value=f"=1/((1+{wacc_cell})^{i})") c.number_format = FMT_DEC; c.font = Font(italic=True, color="595959") df_cells.append(c.coordinate) r += 1 # PV of FCF ws.cell(row=r, column=1, value="Present Value of FCF").font = Font(name="Calibri", bold=True) ws.cell(row=r, column=2, value="--").alignment = RIGHT pv_fcf_cells = [] for i in range(1, 6): c = ws.cell(row=r, column=2+i, value=f"={fcf_cells[i-1]}*{df_cells[i-1]}") c.number_format = FMT_MONEY; c.font = Font(name="Calibri", bold=True) pv_fcf_cells.append(c.coordinate) sum_pv_fcf_cell = f"SUM({pv_fcf_cells[0]}:{pv_fcf_cells[-1]})" r += 2 # --- Valuation Bridge --- _sec(ws, r, "3. Valuation Bridge", 3) r += 1 # TV ws.cell(row=r, column=1, value="Terminal Value (TV)").font = LBL_FONT tv_formula = f"={fcf_cells[-1]}*(1+{tg_cell})/({wacc_cell}-{tg_cell})" c = ws.cell(row=r, column=2, value=tv_formula); c.number_format = FMT_MONEY; c.font = DAT_FONT tv_cell = c.coordinate r += 1 # PV of TV ws.cell(row=r, column=1, value="PV of Terminal Value").font = LBL_FONT pv_tv_formula = f"={tv_cell}*{df_cells[-1]}" c = ws.cell(row=r, column=2, value=pv_tv_formula); c.number_format = FMT_MONEY; c.font = DAT_FONT pv_tv_cell = c.coordinate r += 1 # EV ws.cell(row=r, column=1, value="Implied Enterprise Value").font = Font(name="Calibri", bold=True, size=12) ev_formula = f"={sum_pv_fcf_cell}+{pv_tv_cell}" c = ws.cell(row=r, column=2, value=ev_formula); c.number_format = FMT_MONEY; c.font = Font(name="Calibri", bold=True, size=12) ev_cell = c.coordinate r += 1 # Net Debt ws.cell(row=r, column=1, value="Less: Net Debt").font = LBL_FONT c = ws.cell(row=r, column=2, value=net_debt); c.number_format = FMT_MONEY; c.font = DAT_FONT nd_cell = c.coordinate r += 1 # Equity Value ws.cell(row=r, column=1, value="Implied Equity Value").font = Font(name="Calibri", bold=True, size=12) eq_formula = f"={ev_cell}-{nd_cell}" c = ws.cell(row=r, column=2, value=eq_formula); c.number_format = FMT_MONEY; c.font = Font(name="Calibri", bold=True, size=12) eq_cell = c.coordinate r += 1 # Shares ws.cell(row=r, column=1, value="Diluted Shares Outstanding").font = LBL_FONT c = ws.cell(row=r, column=2, value=shares_out); c.number_format = FMT_DEC; c.font = DAT_FONT shares_cell = c.coordinate r += 1 # Implied Share Price ws.cell(row=r, column=1, value="Implied Share Price").font = Font(name="Calibri", bold=True, size=14, color="1F4E78") share_price_formula = f"={eq_cell}/{shares_cell}" c = ws.cell(row=r, column=2, value=share_price_formula); c.number_format = FMT_DEC; c.font = Font(name="Calibri", bold=True, size=14, color="1F4E78") c.fill = GRN_FILL r += 2 # Format and fit _widths(ws, [35, 18, 18, 18, 18, 18, 18]) def _narrative_sheet(wb, data): ws = wb.create_sheet("Industry & AI Insights") _title(ws, "AI Narrative Analysis", "Generated by AI Models") r = 4 exec_summary = data.get("executive_summary", "") if exec_summary: _sec(ws, r, "Executive Summary", 2); r += 1 c = ws.cell(row=r, column=1, value=exec_summary) c.font = LBL_FONT; c.alignment = Alignment(wrap_text=True, vertical="top") ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=4) ws.row_dimensions[r].height = 100 r += 2 industry_trends = data.get("industry_trends", "") if industry_trends: _sec(ws, r, "Industry Trends", 2); r += 1 c = ws.cell(row=r, column=1, value=industry_trends) c.font = LBL_FONT; c.alignment = Alignment(wrap_text=True, vertical="top") ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=4) ws.row_dimensions[r].height = 120 r += 2 _widths(ws, [100]) def _sources_sheet(wb, data): ws = wb.create_sheet("Sources") _title(ws, "Sources", "All data sources and retrieval dates") _hdr(ws, 4, ["#","URL","Description","Retrieved"]) r = 5 for i, src in enumerate(data.get("sources",[]), start=1): ws.cell(row=r, column=1, value=i).alignment = CENTER url = src.get("url","") uc = ws.cell(row=r, column=2, value=url) uc.font = Font(name="Calibri", size=10, color="0563C1", underline="single") uc.alignment = LEFT if url: try: uc.hyperlink = url except: pass ws.cell(row=r, column=3, value=src.get("description","")).alignment = LEFT ws.cell(row=r, column=4, value=src.get("retrieved","")).alignment = CENTER for col in range(1,5): ws.cell(row=r,column=col).border = THIN ws.row_dimensions[r].height = 28; r += 1 _widths(ws, [6,70,50,16]) # -- Orchestrator -------------------------------------------------------------- def build_workbook(data: dict, output_path: str) -> None: wb = Workbook() wb.remove(wb.active) _summary_sheet(wb, data) _income_sheet(wb, data) _balance_sheet(wb, data) _cashflow_sheet(wb, data) _ratios_sheet(wb, data) _dupont_sheet(wb, data) _dcf_sheet(wb, data) _growth_sheet(wb, data) _peers_sheet(wb, data) _swot_sheet(wb, data) _sources_sheet(wb, data) wb.save(output_path) if __name__ == "__main__": import argparse p = argparse.ArgumentParser() p.add_argument("--input", "-i", required=True) p.add_argument("--output", "-o", required=True) args = p.parse_args() with open(args.input, encoding="utf-8") as f: data = json.load(f) Path(args.output).parent.mkdir(parents=True, exist_ok=True) build_workbook(data, args.output) print(f"Wrote: {args.output}")