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>
This commit is contained in:
Benjamin Admin
2026-05-20 16:23:12 +02:00
parent 7d721a6787
commit 01e2e0fc4b
18 changed files with 1857 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""Add price-escalation formulas to Finanzplan Excel files.
Replaces hard-coded Preis/Monat values in 'Umsatzerlöse' rows 4, 7, 10 with:
- Starting price (Aug 2026) read from a Treiber driver
- Annual price increase applied in January of each subsequent year
- Increase percentage configurable per year via Treiber driver
Treiber layout (appended after existing rows):
blank
'Preise'
'Startpreis/Monat Starter' → 300
'Startpreis/Monat Professional' → 2083
'Startpreis/Monat Enterprise' → 4167
blank
'Preiserhöhung pro Jahr (%)'
'Preiserhöhung 2027' → 0.03
'Preiserhöhung 2028' → 0.03
'Preiserhöhung 2029' → 0.03
'Preiserhöhung 2030' → 0.03
Usage:
python3 pitch-deck/scripts/add-price-formulas.py --dry-run
python3 pitch-deck/scripts/add-price-formulas.py
python3 pitch-deck/scripts/add-price-formulas.py --only Finanzplan-Wandeldarlehen-400k.xlsx
"""
from __future__ import annotations
import argparse
import shutil
import sys
from datetime import datetime
from pathlib import Path
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
EXPORTS = Path(__file__).resolve().parent.parent / "exports"
# Segment → row in Umsatzerlöse where Preis/Monat lives
PRICE_ROWS = {
"starter": 4,
"professional": 7,
"enterprise": 10,
}
# Default starting prices (Aug 2026) — used only if existing value isn't a number
DEFAULT_START_PRICES = {
"starter": 300,
"professional": 2083,
"enterprise": 4167,
}
DEFAULT_PRICE_INCREASE = 0.03 # 3% per year by default
_SEG_LABEL = {"starter": "Starter", "professional": "Professional", "enterprise": "Enterprise"}
def year_columns(ws_kunden) -> dict[int, list[int]]:
out: dict[int, list[int]] = {}
for c in range(2, ws_kunden.max_column + 1):
v = ws_kunden.cell(row=1, column=c).value
if v is None:
continue
try:
yr = int(v)
except (TypeError, ValueError):
continue
out.setdefault(yr, []).append(c)
return out
def existing_start_prices(ws_umsatz) -> dict[str, float]:
"""Read current Aug 2026 (column B) prices to use as Startpreis defaults."""
res: dict[str, float] = {}
for suffix, row in PRICE_ROWS.items():
v = ws_umsatz.cell(row=row, column=2).value
try:
res[suffix] = float(v) if v is not None else float(DEFAULT_START_PRICES[suffix])
except (TypeError, ValueError):
# Already a formula? Use default.
res[suffix] = float(DEFAULT_START_PRICES[suffix])
return res
def already_processed(ws_treiber) -> tuple[bool, int]:
"""Return (already_processed, start_row). start_row is where to place the Preise block."""
# Scan for an existing 'Preise' header
for r in range(1, ws_treiber.max_row + 1):
if ws_treiber.cell(row=r, column=1).value == "Preise":
return True, r
# First time: place after max_row with a blank separator
return False, ws_treiber.max_row + 2
def read_existing_price_drivers(ws_treiber, header_row: int, years_after_start: list[int]) -> tuple[dict, dict, dict]:
"""Read existing price drivers we wrote previously. Returns (start_prices, increases, refs)."""
start_prices: dict[str, float] = {}
increases: dict[int, float] = {}
refs: dict[str, int | dict[int, int]] = {}
r = header_row + 1
start_refs: dict[str, int] = {}
for suffix in ("starter", "professional", "enterprise"):
v = ws_treiber.cell(row=r, column=2).value
try:
start_prices[suffix] = float(v) if v is not None else float(DEFAULT_START_PRICES[suffix])
except (TypeError, ValueError):
start_prices[suffix] = float(DEFAULT_START_PRICES[suffix])
start_refs[suffix] = r
r += 1
refs["start_refs"] = start_refs
r += 1 # blank
r += 1 # 'Preiserhöhung' header
increase_refs: dict[int, int] = {}
for yr in years_after_start:
v = ws_treiber.cell(row=r, column=2).value
try:
increases[yr] = float(v) if v is not None else DEFAULT_PRICE_INCREASE
except (TypeError, ValueError):
increases[yr] = DEFAULT_PRICE_INCREASE
increase_refs[yr] = r
r += 1
refs["increase_refs"] = increase_refs
return start_prices, increases, refs
def write_treiber_price_drivers(ws_treiber, start_row: int, years_after_start: list[int],
start_prices: dict[str, float], increases: dict[int, float]) -> dict:
r = start_row
ws_treiber.cell(row=r, column=1).value = "Preise"
r += 1
start_refs: dict[str, int] = {}
for suffix in ("starter", "professional", "enterprise"):
ws_treiber.cell(row=r, column=1).value = f"Startpreis/Monat {_SEG_LABEL[suffix]}"
ws_treiber.cell(row=r, column=2).value = start_prices[suffix]
start_refs[suffix] = r
r += 1
r += 1 # blank
ws_treiber.cell(row=r, column=1).value = "Preiserhöhung pro Jahr (%)"
r += 1
increase_refs: dict[int, int] = {}
for yr in years_after_start:
ws_treiber.cell(row=r, column=1).value = f"Preiserhöhung {yr}"
ws_treiber.cell(row=r, column=2).value = increases.get(yr, DEFAULT_PRICE_INCREASE)
increase_refs[yr] = r
r += 1
return {"start_refs": start_refs, "increase_refs": increase_refs}
def write_umsatz_price_formulas(ws_umsatz, refs: dict, base_year: int) -> int:
"""Write price formulas to Umsatzerlöse rows 4/7/10 across all month columns.
Formula structure:
col B (Aug 2026, first month): =Treiber!$B$startpreis
col C+ same year: =prev_col
col where month=1 and year>base_year:
=prev_col*(1+INDEX(Treiber!$B$inc_first:$B$inc_last,year-base_year))
"""
start_refs = refs["start_refs"]
increase_refs = refs["increase_refs"]
inc_first = min(increase_refs.values())
inc_last = max(increase_refs.values())
cells_written = 0
for suffix, row in PRICE_ROWS.items():
# Clean label
ws_umsatz.cell(row=row, column=1).value = f"Preis/Monat ({_SEG_LABEL[suffix]})"
for c in range(2, ws_umsatz.max_column + 1):
col_letter = get_column_letter(c)
if c == 2:
# Aug 2026 starting price
ws_umsatz.cell(row=row, column=c).value = (
f"=Treiber!$B${start_refs[suffix]}"
)
else:
prev_col = get_column_letter(c - 1)
ws_umsatz.cell(row=row, column=c).value = (
f"=IF(AND({col_letter}$2=1,{col_letter}$1>{base_year}),"
f"{prev_col}{row}*(1+INDEX(Treiber!$B${inc_first}:$B${inc_last},"
f"{col_letter}$1-{base_year})),"
f"{prev_col}{row})"
)
cells_written += 1
return cells_written
def process_file(path: Path, dry_run: bool) -> dict | None:
wb = load_workbook(path)
if "Umsatzerlöse" not in wb.sheetnames or "Treiber" not in wb.sheetnames or "Kunden" not in wb.sheetnames:
return None
ws_u = wb["Umsatzerlöse"]
ws_t = wb["Treiber"]
ws_k = wb["Kunden"]
yc = year_columns(ws_k)
years = sorted(yc.keys())
base_year = min(years)
years_after_start = [y for y in years if y > base_year]
processed, start_row = already_processed(ws_t)
if processed:
# Preserve existing driver values
start_prices, increases, _ = read_existing_price_drivers(ws_t, start_row, years_after_start)
source = "treiber (preserved)"
else:
# First time: read current Aug 2026 prices, default increases
start_prices = existing_start_prices(ws_u)
increases = {y: DEFAULT_PRICE_INCREASE for y in years_after_start}
source = "umsatz column B"
refs = write_treiber_price_drivers(ws_t, start_row, years_after_start, start_prices, increases)
cells = write_umsatz_price_formulas(ws_u, refs, base_year)
if not dry_run:
wb.save(path)
return {
"years": years,
"start_prices": start_prices,
"increases": increases,
"cells": cells,
"source": source,
"refs": refs,
}
def backup(path: Path) -> Path:
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
bk = path.with_name(f"{path.stem}.BACKUP-pre-price-formulas-{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("--only", help="Process only this filename")
ap.add_argument("--no-backup", action="store_true")
args = ap.parse_args()
files = sorted(EXPORTS.glob("Finanzplan-*.xlsx"))
files = [f for f in files if "BACKUP" not in f.name]
if args.only:
files = [f for f in files if f.name == args.only]
for path in files:
wb_peek = load_workbook(path, read_only=True)
if not all(s in wb_peek.sheetnames for s in ("Umsatzerlöse", "Treiber", "Kunden")):
print(f"\n skip {path.name}: missing required sheets")
continue
if not args.dry_run and not args.no_backup:
bk = backup(path)
print(f" ✓ backup: {bk.name}")
info = process_file(path, dry_run=args.dry_run)
if info is None:
continue
print(f"\n === {path.name} ===")
print(f" Source: {info['source']}")
print(f" Startpreise (Aug {min(info['years'])}):")
for seg, val in info["start_prices"].items():
print(f" {seg:13s}: {val:>8.2f} €/Monat")
print(" Preiserhöhung pro Jahr (Jan jeweils):")
for yr, inc in info["increases"].items():
print(f" {yr}: {inc*100:.1f}%")
print(f" Cells written: {info['cells']}")
return 0
if __name__ == "__main__":
sys.exit(main())