Files
breakpilot-core/pitch-deck/scripts/copy-extra-sheets.py
T
Benjamin Admin 01e2e0fc4b feat(pitch-deck): Finanzplan-Tooling + formel-getriebene Versionen Base/Bull/Bear
8 neue Skripte erweitern die Excel-Finanzpläne deutlich:
- add-kunden-formulas: Neukunden-Lookup + kumulativer Churn (SUMPRODUCT-basiert)
- add-price-formulas: jährliche Preiserhöhung Jan via Treiber
- add-inflation-formulas: Inflation auf Betriebskosten + Büromiete-Logik
- add-tantieme-and-explanations: Gründer-Tantieme 2028-2030 + Erläuterungen
  in Cohort-Analyse + Sensitivity-Sheets
- apply-bueromiete: 1000€/Monat ab Sep 2026 mit Inflation
- apply-number-formatting: Euro / Count / Percent per Label-Klassifikation
- cleanup-finanzplan-labels: 'kategorie — '-Präfix entfernt
- copy-extra-sheets: Charts/Cohort/Sensitivity/Hiring-Plan von Series-A
  auf 400k Base/Bull/Bear übertragen (inkl. 12 Chart-Objekten)

Neue Excel-Dateien (für L-Bank Wandeldarlehen 400k Pitch):
- Finanzplan-Wandeldarlehen-400k.xlsx (Base)
- Finanzplan-Wandeldarlehen-400k-Bull.xlsx
- Finanzplan-Wandeldarlehen-400k-Bear.xlsx
- Finanzplan-Series-A-Ambitioniert.xlsx (Series-A Variante)

Inhaltliche Anpassungen (400k Base/Bull/Bear):
- Channel-Provision Bechtle/Cancom → Channel-Partner Provision, Format Euro
- GuV: 'Steuerbares Einkommen' → 'Zu versteuerndes Einkommen (nach Verlustvortrag)',
  Formel um Zinserträge/-aufwand erweitert
- IT-Recht/Datenschutzjurist auf 100% (6666 € statt 3333 €)
- Series-A-Investor in WD-Sheet auf 0 € (nicht eingeplant in 400k Variante)
- Mitarbeiter +1 Monat verschoben (außer Gründer = Okt 2026)
- 3 Enterprise-Neukunden zusätzlich (Apr 2027, Jun 2027, Okt 2029)
- Marketing-Agentur Cut ~33% pro Szenario (Base 4%, Bull 5%, Bear 2%)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:23:12 +02:00

226 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())