#!/usr/bin/env python3 """Copy 'extra' sheets from the Series-A Ambitioniert reference workbook to the 400k Wandeldarlehen Base/Bull/Bear workbooks. Source: Finanzplan-Series-A-Ambitioniert.xlsx Sheets copied: - Charts (153 rows + 12 chart objects) - Unit Economics (Investor-KPIs incl. LTV/CAC, NRR) - Wandeldarlehen (Tranchen-Konditionen) - Cohort-Analyse (Retention pro Akquise-Monat) - Sensitivity (Sensitivity-Analyse) - Hiring-Plan (Quartal-Übersicht) Also renames 'Net Dollar Retention' → 'Net Revenue Retention' and 'NDR' → 'NRR' across all target files (and the source), since we calculate in Euro. Cell values, number formats, basic styles, merged cells, column widths, row heights, and freeze panes are copied. Charts are deep-copied — they may fail silently if the openpyxl chart structure isn't fully serializable; a warning is printed in that case. Usage: python3 pitch-deck/scripts/copy-extra-sheets.py --dry-run python3 pitch-deck/scripts/copy-extra-sheets.py """ from __future__ import annotations import argparse import shutil import sys from copy import copy as shallow_copy from copy import deepcopy from datetime import datetime from pathlib import Path from openpyxl import load_workbook EXPORTS = Path(__file__).resolve().parent.parent / "exports" SOURCE_FILE = "Finanzplan-Series-A-Ambitioniert.xlsx" TARGETS = [ "Finanzplan-Wandeldarlehen-400k.xlsx", "Finanzplan-Wandeldarlehen-400k-Bull.xlsx", "Finanzplan-Wandeldarlehen-400k-Bear.xlsx", ] SHEETS_TO_COPY = [ "Charts", "Unit Economics", "Wandeldarlehen", "Cohort-Analyse", "Sensitivity", "Hiring-Plan", ] # Currency-neutral renames (Euro project, not USD) RENAMES = { "Net Dollar Retention": "Net Revenue Retention", "NDR Annahme": "NRR Annahme", "• NDR steigt": "• NRR steigt", "NDR": "NRR", # last; safety renames any remaining bare 'NDR' } def copy_cell_style(src_cell, dst_cell) -> None: if src_cell.has_style: dst_cell.font = shallow_copy(src_cell.font) dst_cell.fill = shallow_copy(src_cell.fill) dst_cell.border = shallow_copy(src_cell.border) dst_cell.alignment = shallow_copy(src_cell.alignment) dst_cell.number_format = src_cell.number_format dst_cell.protection = shallow_copy(src_cell.protection) def copy_sheet_content(src_ws, dst_ws) -> None: """Copy cells, dimensions, merged ranges, freeze panes, and charts.""" for row in src_ws.iter_rows(): for src_cell in row: dst_cell = dst_ws.cell(row=src_cell.row, column=src_cell.column, value=src_cell.value) copy_cell_style(src_cell, dst_cell) # Column widths + hidden state for col_key, dim in src_ws.column_dimensions.items(): d = dst_ws.column_dimensions[col_key] if dim.width: d.width = dim.width d.hidden = dim.hidden # Row heights + hidden state for row_key, dim in src_ws.row_dimensions.items(): d = dst_ws.row_dimensions[row_key] if dim.height: d.height = dim.height d.hidden = dim.hidden # Merged cells for merged_range in list(src_ws.merged_cells.ranges): dst_ws.merge_cells(str(merged_range)) # Freeze panes if src_ws.freeze_panes: dst_ws.freeze_panes = src_ws.freeze_panes # Charts (best-effort deepcopy) charts_ok = 0 charts_fail = 0 for chart in src_ws._charts: try: new_chart = deepcopy(chart) dst_ws.add_chart(new_chart) charts_ok += 1 except Exception as e: # noqa: BLE001 charts_fail += 1 print(f" ⚠ chart copy failed: {type(e).__name__}: {e}", file=sys.stderr) if charts_ok or charts_fail: print(f" charts: {charts_ok} ok, {charts_fail} failed") def insert_sheet_before_formeluebersicht(wb, sheet_name: str) -> None: """If 'Formelübersicht' is in the workbook, move new sheet just before it.""" if "Formelübersicht" not in wb.sheetnames: return new_idx = wb.sheetnames.index(sheet_name) fmt_idx = wb.sheetnames.index("Formelübersicht") if new_idx > fmt_idx: # move new sheet to just before Formelübersicht offset = (fmt_idx) - new_idx wb.move_sheet(sheet_name, offset=offset) def rename_in_workbook(wb) -> int: """Apply NDR → NRR renames across all string cells. Returns count of changes.""" n = 0 for ws in wb.worksheets: for row in ws.iter_rows(): for cell in row: if not isinstance(cell.value, str) or not cell.value: continue new_value = cell.value for old, new in RENAMES.items(): if old in new_value: new_value = new_value.replace(old, new) if new_value != cell.value: cell.value = new_value n += 1 return n def process_target(target_path: Path, src_wb, dry_run: bool) -> dict: wb = load_workbook(target_path) copied_sheets = [] for sheet_name in SHEETS_TO_COPY: if sheet_name not in src_wb.sheetnames: print(f" skip {sheet_name}: not in source") continue if sheet_name in wb.sheetnames: del wb[sheet_name] new_ws = wb.create_sheet(title=sheet_name) print(f" copying {sheet_name} ...") copy_sheet_content(src_wb[sheet_name], new_ws) insert_sheet_before_formeluebersicht(wb, sheet_name) copied_sheets.append(sheet_name) renames = rename_in_workbook(wb) if not dry_run: wb.save(target_path) return {"copied": copied_sheets, "renames": renames} def backup(path: Path) -> Path: ts = datetime.now().strftime("%Y%m%d-%H%M%S") bk = path.with_name(f"{path.stem}.BACKUP-pre-extra-sheets-{ts}{path.suffix}") shutil.copy2(path, bk) return bk def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--dry-run", action="store_true") ap.add_argument("--no-backup", action="store_true") ap.add_argument("--skip-rename", action="store_true", help="Don't apply NDR → NRR renames") args = ap.parse_args() if args.skip_rename: RENAMES.clear() source_path = EXPORTS / SOURCE_FILE if not source_path.exists(): print(f"ERROR: source not found: {source_path}", file=sys.stderr) return 2 src_wb = load_workbook(source_path) # Also rename in source if not args.dry_run: bk_src = backup(source_path) print(f" ✓ source backup: {bk_src.name}") n_src_renames = rename_in_workbook(src_wb) if not args.dry_run: src_wb.save(source_path) print(f" source ({SOURCE_FILE}): {n_src_renames} cell rename(s)") # Re-open source as the now-renamed version so target copies pick up new labels src_wb = load_workbook(source_path) for name in TARGETS: path = EXPORTS / name if not path.exists(): print(f"\n ⨯ skip {name}: not found") continue if not args.dry_run and not args.no_backup: bk = backup(path) print(f"\n ✓ backup: {bk.name}") print(f" === {name} ===") info = process_target(path, src_wb, dry_run=args.dry_run) print(f" copied: {info['copied']}") print(f" cell renames: {info['renames']}") return 0 if __name__ == "__main__": sys.exit(main())