#!/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())