#!/usr/bin/env python3 """ Build MPM advertising-financed ROI cap table workbooks. Three model variants, each written as its own workbook: 2A Internal — MPM Financed, ROI & Payback MPM 30 / Passent 30 / Reseller 10 / End User 30. Carries COGS, sale-price and MSRP milestones against MPM's share, with interest accruing on the financed capital. 2B Internal — MPM Financed, Revenue Distribution Same split, no milestones and no cost basis. A straight projection of what each party earns. 2C Customer-Financed Acquisition MPM 5 / Reseller 2.5 / Passent 22.5 / End User 70. No MPM capital at risk, so no COGS or sale-price milestone and no interest. Carries the MSRP milestone against the End User's share — when the customer's 70% pays back what they bought at list. Usage: python3 build_cap_table.py config.json --model all --outdir ./out python3 build_cap_table.py config.json --model 2A -o internal_roi.xlsx See references/config_schema.md for the config format. """ import argparse import copy import json import os import sys from datetime import date from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter # -------------------------------------------------------------------------- # Model presets # -------------------------------------------------------------------------- MODELS = { "2A": { "key": "2A", "title": "Internal — MPM Financed (ROI & Payback)", "audience": "Internal. Contains cost and margin data.", "shares": {"MPM": 0.30, "Passent": 0.30, "Reseller": 0.10, "End User": 0.30}, "milestones": ["cogs", "sale", "msrp"], "milestone_party": "MPM", "interest": True, "recurring_borne_by": "MPM", "show_cost_basis": True, }, "2B": { "key": "2B", "title": "Internal — MPM Financed (Revenue Distribution)", "audience": "Internal. Revenue only — no cost, margin or payback data.", "shares": {"MPM": 0.30, "Passent": 0.30, "Reseller": 0.10, "End User": 0.30}, "milestones": [], "milestone_party": None, "interest": False, "recurring_borne_by": "MPM", "show_cost_basis": False, }, "2C": { "key": "2C", "title": "Customer-Financed Acquisition", "audience": "Customer-facing. No COGS, margin or sale-price data.", "shares": {"MPM": 0.05, "Reseller": 0.025, "Passent": 0.225, "End User": 0.70}, "milestones": ["msrp"], "milestone_party": "End User", "interest": False, "recurring_borne_by": "End User", "show_cost_basis": False, }, } # Which cost basis a party is charged recurring licensing at. MPM pays its # vendor cost; the End User pays the invoiced price. RECURRING_BASIS = {"MPM": "cogs", "End User": "net", "Customer": "net"} # -------------------------------------------------------------------------- # Styling # -------------------------------------------------------------------------- MPM_GOLD = "C8A951" DARK = "2F2F2F" LIGHT = "F2F2F2" FILL_COGS = PatternFill("solid", fgColor="FFF200") # yellow FILL_SALE = PatternFill("solid", fgColor="FFA64D") # orange FILL_MSRP = PatternFill("solid", fgColor="7ED957") # green MILESTONE_FILL = {"cogs": FILL_COGS, "sale": FILL_SALE, "msrp": FILL_MSRP} MILESTONE_LABEL = {"cogs": "COGS", "sale": "Sale Price", "msrp": "MSRP"} HDR_FILL = PatternFill("solid", fgColor=DARK) HDR_FONT = Font(bold=True, color="FFFFFF", size=10) SUB_FILL = PatternFill("solid", fgColor=MPM_GOLD) SUB_FONT = Font(bold=True, color="000000", size=10) TITLE_FONT = Font(bold=True, size=14) LABEL_FONT = Font(bold=True, size=10) NOTE_FONT = Font(italic=True, size=9, color="666666") THIN = Side(style="thin", color="BFBFBF") BOX = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) MONEY = '"$"#,##0.00' MONEY0 = '"$"#,##0' PCT = "0.0%" # -------------------------------------------------------------------------- # Model # -------------------------------------------------------------------------- def month_label(start_ym, offset): y, m = (int(x) for x in start_ym.split("-")[:2]) total = (y * 12 + (m - 1)) + offset return f"{total // 12}-{total % 12 + 1:02d}" def ramp_factor(month, ramp_months, start_pct): if ramp_months <= 1 or month >= ramp_months: return 1.0 return start_pct + (1.0 - start_pct) * (month - 1) / (ramp_months - 1) def line_totals(lines): msrp = sum(l["msrp_unit"] * l["qty"] for l in lines) net = sum(l["net_subtotal"] for l in lines) cogs = sum(l["net_subtotal"] - l["margin"] for l in lines) return msrp, net, cogs def resolve_model(cfg, key): """Preset merged with any per-model overrides in the config.""" spec = copy.deepcopy(MODELS[key]) override = (cfg.get("models") or {}).get(key, {}) spec.update(override) total = sum(spec["shares"].values()) if abs(total - 1.0) > 1e-6: raise ValueError(f"Model {key} shares sum to {total:.4f}, not 1.0 — " f"every dollar of gross must be allocated.") return spec def build_model(cfg, model_key): spec = resolve_model(cfg, model_key) horizon = cfg.get("horizon_months", 60) ramp_months = cfg.get("ramp_months", 12) ramp_start = cfg.get("ramp_start_pct", 0.25) monthly_gross = cfg["ad_revenue"]["monthly_gross"] start_ym = cfg.get("start_date", "2027-01") lines = cfg["lines"] one_time = [l for l in lines if not l.get("recurring")] recurring = [l for l in lines if l.get("recurring")] ot_msrp, ot_net, ot_cogs = line_totals(one_time) rc_msrp, rc_net, rc_cogs = line_totals(recurring) targets = {"cogs": ot_cogs, "sale": ot_net, "msrp": ot_msrp} # Who pays the ongoing licensing, and at what basis. bearer = spec["recurring_borne_by"] basis = RECURRING_BASIS.get(bearer, "cogs") monthly_recurring = (rc_cogs if basis == "cogs" else rc_net) / 12.0 contributions = {} for c in cfg.get("client_contributions", []): m = int(c["month"]) contributions[m] = contributions.get(m, 0.0) + float(c["amount"]) surge_by_month = {} for s in cfg.get("surges", []): if not s.get("enabled"): continue dur = max(1, int(round(float(s.get("months", 1))))) per = float(s["total"]) / dur for k in range(dur): m = int(s["start_month"]) + k if 1 <= m <= horizon: surge_by_month[m] = surge_by_month.get(m, 0.0) + per # Financing. Principal is MPM's actual cash outlay — the one-time COGS — # not the price it would have sold for. You cannot borrow your own margin. fin = cfg.get("finance", {}) rate = float(fin.get("annual_rate", 0.12)) basis_key = fin.get("principal_basis", "cogs") principal = (float(fin["principal_amount"]) if fin.get("principal_amount") else targets.get(basis_key, ot_cogs)) use_interest = bool(spec["interest"]) and rate > 0 parties = list(spec["shares"].keys()) mparty = spec["milestone_party"] rows = [] cum = {p: 0.0 for p in parties} cum_net = 0.0 # milestone party's cumulative, net of what it bears cum_interest = 0.0 balance = principal if use_interest else 0.0 neg_amort_months = [] for m in range(1, horizon + 1): rf = ramp_factor(m, ramp_months, ramp_start) gross = monthly_gross * rf + surge_by_month.get(m, 0.0) contrib = contributions.get(m, 0.0) row = {"month": m, "date": month_label(start_ym, m - 1), "ramp": rf, "gross": gross, "contribution": contrib, "parties": {}, "cum_parties": {}} for p in parties: amt = gross * spec["shares"][p] cum[p] += amt row["parties"][p] = amt row["cum_parties"][p] = cum[p] # Cost borne by the milestone party (or by MPM in the 2B case, where # there is no milestone but the drag is still worth showing). charge_party = mparty or "MPM" charge = monthly_recurring if charge_party == bearer else 0.0 row["recurring_charge"] = charge row["charged_party"] = charge_party net = row["parties"].get(charge_party, 0.0) + contrib - charge row["net"] = net cum_net += net row["cum_net"] = cum_net if use_interest: interest = balance * rate / 12.0 if balance > 1e-9 else 0.0 balance += interest payment = max(0.0, min(net, balance)) balance -= payment cum_interest += interest if interest > 0 and net < interest: neg_amort_months.append(m) else: interest = 0.0 row["interest"] = interest row["cum_interest"] = cum_interest row["balance"] = max(0.0, balance) if use_interest else None rows.append(row) # A milestone is cleared when the party has recovered the capital *and* # the cost of carrying it to that point. crossings = {} for key in spec["milestones"]: t = targets[key] hit = None for row in rows: if row["cum_net"] >= t + row["cum_interest"]: hit = row["month"] break crossings[key] = hit prop_screens = cfg["ad_revenue"].get("screen_count") quote_screens = sum(l["qty"] for l in lines if l.get("is_display")) pairing = {"proposal": prop_screens, "quote": quote_screens or None, "ok": bool(prop_screens and quote_screens and abs(prop_screens - quote_screens) < 0.5)} return { "cfg": cfg, "spec": spec, "rows": rows, "parties": parties, "targets": targets, "crossings": crossings, "horizon": horizon, "one_time": one_time, "recurring": recurring, "totals": {"one_time": (ot_msrp, ot_net, ot_cogs), "recurring_annual": (rc_msrp, rc_net, rc_cogs), "contract_msrp": ot_msrp + rc_msrp, "contract_net": ot_net + rc_net, "contract_cogs": ot_cogs + rc_cogs}, "monthly_recurring": monthly_recurring, "recurring_bearer": bearer, "finance": {"rate": rate, "principal": principal, "active": use_interest, "total_interest": cum_interest, "neg_amort_months": neg_amort_months}, "pairing": pairing, } # -------------------------------------------------------------------------- # Sheet helpers # -------------------------------------------------------------------------- def _hdr(ws, row, col, text, fill=HDR_FILL, font=HDR_FONT): c = ws.cell(row=row, column=col, value=text) c.fill, c.font, c.border = fill, font, BOX c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) return c def _band(ws, row, text, width): c = ws.cell(row=row, column=1, value=text) c.fill, c.font = SUB_FILL, SUB_FONT ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=width) return c def _note(ws, row, text, width=6, height=28): c = ws.cell(row=row, column=1, value=text) c.font = NOTE_FONT ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=width) c.alignment = Alignment(wrap_text=True, vertical="top") ws.row_dimensions[row].height = height # -------------------------------------------------------------------------- # Summary sheet # -------------------------------------------------------------------------- def write_summary(wb, model): cfg, spec = model["cfg"], model["spec"] ws = wb.create_sheet("Summary", 0) ws.sheet_view.showGridLines = False ws.column_dimensions["A"].width = 40 for col in "BCDEF": ws.column_dimensions[col].width = 18 ws["A1"] = f"Model {spec['key']} — {spec['title']}" ws["A1"].font = TITLE_FONT ws["A2"] = cfg.get("project_name", "") ws["A2"].font = Font(italic=True, size=11) ws["A3"] = spec["audience"] ws["A3"].font = Font(bold=True, size=9, color="B00000") r = 5 for label, val in [ ("Customer", cfg.get("customer", "")), ("Sales Order", cfg.get("sale_order", "")), ("Ad Proposal", cfg.get("ad_proposal_ref", "")), ("Projection Start", cfg.get("start_date", "")), ("Horizon (months)", model["horizon"]), ("Prepared", cfg.get("prepared_on", date.today().isoformat())), ]: ws.cell(row=r, column=1, value=label).font = LABEL_FONT ws.cell(row=r, column=2, value=val) r += 1 pc = model["pairing"] ws.cell(row=r, column=1, value="Screens — proposal vs. quote").font = LABEL_FONT if pc["proposal"] is None or pc["quote"] is None: ws.cell(row=r, column=2, value="not reconciled").font = Font(italic=True, color="999999") elif pc["ok"]: ws.cell(row=r, column=2, value=f"{pc['proposal']:g} / {pc['quote']:g} — match" ).font = Font(color="1F7A1F", bold=True) else: ws.cell(row=r, column=2, value=f"{pc['proposal']:g} / {pc['quote']:g} — MISMATCH, verify scope" ).font = Font(color="B00000", bold=True) r += 1 _note(ws, r, "This model applies one advertising proposal to one sales order. Its revenue " "figures are specific to this deployment and are not transferable to any " "other quote.", 6, 26) r += 2 # --- Distribution ---------------------------------------------------- _band(ws, r, "Revenue Distribution", 6) r += 1 for i, h in enumerate(["Party", "Share", "Steady-State / mo", "Year 1", f"{model['horizon']}-Month Total"], start=1): _hdr(ws, r, i, h) r += 1 last = model["rows"][-1] yr1 = min(12, model["horizon"]) - 1 gross_ss = cfg["ad_revenue"]["monthly_gross"] for p in model["parties"]: share = spec["shares"][p] ws.cell(row=r, column=1, value=p).font = LABEL_FONT c = ws.cell(row=r, column=2, value=share); c.number_format = PCT c = ws.cell(row=r, column=3, value=gross_ss * share); c.number_format = MONEY c = ws.cell(row=r, column=4, value=model["rows"][yr1]["cum_parties"][p]); c.number_format = MONEY c = ws.cell(row=r, column=5, value=last["cum_parties"][p]); c.number_format = MONEY if p == "MPM": for col in range(1, 6): ws.cell(row=r, column=col).fill = PatternFill("solid", fgColor="FFF9E0") r += 1 ws.cell(row=r, column=1, value="Total gross").font = LABEL_FONT c = ws.cell(row=r, column=2, value=1.0); c.number_format = PCT c = ws.cell(row=r, column=3, value=gross_ss); c.number_format = MONEY c = ws.cell(row=r, column=4, value=sum(model["rows"][yr1]["cum_parties"].values())); c.number_format = MONEY c = ws.cell(row=r, column=5, value=sum(last["cum_parties"].values())); c.number_format = MONEY for col in range(1, 6): ws.cell(row=r, column=col).font = LABEL_FONT ws.cell(row=r, column=col).border = Border(top=THIN) r += 2 # --- Financing ------------------------------------------------------- fin = model["finance"] if fin["active"]: _band(ws, r, "Financed Capital and Cost of Money", 6) r += 1 payoff = model["crossings"].get("cogs") for label, val, fmt in [ ("Principal financed (one-time COGS)", fin["principal"], MONEY), ("Annual rate", fin["rate"], PCT), ("Method", "Declining balance; MPM's share services interest first", None), ("Total interest accrued", fin["total_interest"], MONEY), ("Balance retired in month", (f"Month {payoff}" if payoff else f">{model['horizon']} months"), None), ]: ws.cell(row=r, column=1, value=label).font = LABEL_FONT c = ws.cell(row=r, column=2, value=val) if fmt: c.number_format = fmt r += 1 if fin["neg_amort_months"]: ws.cell(row=r, column=1, value=f"Negative amortization in months " f"{fin['neg_amort_months'][0]}–{fin['neg_amort_months'][-1]}: " f"MPM's share does not cover accruing interest during the ramp, " f"so the balance grows before it falls.").font = \ Font(size=9, color="B00000") ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6) ws.cell(row=r, column=1).alignment = Alignment(wrap_text=True, vertical="top") ws.row_dimensions[r].height = 28 r += 1 r += 1 # --- Milestones ------------------------------------------------------ if spec["milestones"]: mparty = spec["milestone_party"] _band(ws, r, f"Payoff Milestones — recovered from {mparty}'s share", 6) r += 1 for i, h in enumerate(["Milestone", "Target", "Reached", "Fill"], start=1): _hdr(ws, r, i, h) r += 1 for key in spec["milestones"]: hit = model["crossings"][key] ws.cell(row=r, column=1, value=MILESTONE_LABEL[key]).font = LABEL_FONT c = ws.cell(row=r, column=2, value=model["targets"][key]); c.number_format = MONEY c = ws.cell(row=r, column=3, value=(f"Month {hit}" if hit else f">{model['horizon']} months")) c.alignment = Alignment(horizontal="center") if not hit: c.font = Font(italic=True, color="B00000") ws.cell(row=r, column=4).fill = MILESTONE_FILL[key] ws.cell(row=r, column=4).border = BOX r += 1 if fin["active"]: _note(ws, r, "Milestones clear when cumulative net recovers the target plus the " "interest accrued to that month, so they sit later than they would " "on a zero-cost-of-capital basis.", 6, 26) r += 1 r += 1 # --- ROI --------------------------------------------------------- _band(ws, r, f"Return at Month {model['horizon']} — {mparty}", 6) r += 1 cols = ["Basis", "Target", "Cumulative Net"] if fin["active"]: cols.append("Net of Interest") cols.append("ROI") for i, h in enumerate(cols, start=1): _hdr(ws, r, i, h) r += 1 cum_final = last["cum_net"] net_of_int = cum_final - fin["total_interest"] for key in spec["milestones"]: t = model["targets"][key] ws.cell(row=r, column=1, value=MILESTONE_LABEL[key]).font = LABEL_FONT c = ws.cell(row=r, column=2, value=t); c.number_format = MONEY c = ws.cell(row=r, column=3, value=cum_final); c.number_format = MONEY col = 4 if fin["active"]: c = ws.cell(row=r, column=4, value=net_of_int); c.number_format = MONEY col = 5 c = ws.cell(row=r, column=col, value=(net_of_int - t) / t if t else 0) c.number_format = PCT c.font = Font(color="1F7A1F" if net_of_int >= t else "B00000", bold=True) r += 1 if spec["key"] == "2C": _note(ws, r, "This is the End User's return on the system they purchased at list, " "funded by their 70% share of advertising revenue. It is not MPM's " "return — MPM's position in this model is its 5% share, shown above.", 6, 26) r += 1 r += 1 if not spec["milestones"]: _note(ws, r, "This model is a revenue distribution projection only. It carries no cost " "basis, no payback milestone and no margin data by design.", 6, 26) r += 2 return ws # -------------------------------------------------------------------------- # Monthly projection sheet # -------------------------------------------------------------------------- def write_projection(wb, model): spec = model["spec"] ws = wb.create_sheet("Monthly Projection") ws.sheet_view.showGridLines = False ws["A1"] = f"{model['horizon']}-Month Projection — Model {spec['key']}" ws["A1"].font = TITLE_FONT if spec["milestones"]: col = 1 for key in spec["milestones"]: c = ws.cell(row=2, column=col, value=f"{MILESTONE_LABEL[key]} recovered") c.fill, c.font, c.border = MILESTONE_FILL[key], Font(bold=True, size=9), BOX c.alignment = Alignment(horizontal="center") col += 2 headers = ["Month", "Date", "Ramp %", "Gross Ad Revenue"] headers += [f"{p} ({spec['shares'][p]:.1%})" for p in model["parties"]] headers += [f"{p} cum." for p in model["parties"]] mparty = spec["milestone_party"] or "MPM" detail_start = len(headers) + 1 if spec["milestones"] or model["monthly_recurring"]: headers.append(f"{mparty} recurring cost") headers.append(f"{mparty} net") if spec["interest"]: headers += ["Interest", "Balance"] cum_net_col = None if spec["milestones"]: headers.append(f"{mparty} cumulative net") cum_net_col = len(headers) hdr_row = 4 for i, h in enumerate(headers, start=1): _hdr(ws, hdr_row, i, h) # Band the party columns so the split reads at a glance. ws.merge_cells(start_row=hdr_row - 1, start_column=5, end_row=hdr_row - 1, end_column=4 + 2 * len(model["parties"])) c = ws.cell(row=hdr_row - 1, column=5, value="Revenue Distribution (monthly, then cumulative)") c.fill, c.font = SUB_FILL, SUB_FONT c.alignment = Alignment(horizontal="center") if detail_start <= len(headers): ws.merge_cells(start_row=hdr_row - 1, start_column=detail_start, end_row=hdr_row - 1, end_column=len(headers)) c = ws.cell(row=hdr_row - 1, column=detail_start, value=f"{mparty} Position" + (" and Debt Service" if spec["interest"] else "")) c.fill, c.font = SUB_FILL, SUB_FONT c.alignment = Alignment(horizontal="center") first = hdr_row + 1 np_ = len(model["parties"]) for idx, row in enumerate(model["rows"]): r = first + idx ws.cell(row=r, column=1, value=row["month"]).alignment = Alignment(horizontal="center") ws.cell(row=r, column=2, value=row["date"]).alignment = Alignment(horizontal="center") c = ws.cell(row=r, column=3, value=row["ramp"]); c.number_format = PCT c = ws.cell(row=r, column=4, value=row["gross"]); c.number_format = MONEY0 for j, p in enumerate(model["parties"]): c = ws.cell(row=r, column=5 + j, value=row["parties"][p]); c.number_format = MONEY0 c = ws.cell(row=r, column=5 + np_ + j, value=row["cum_parties"][p]) c.number_format = MONEY0 col = detail_start if spec["milestones"] or model["monthly_recurring"]: c = ws.cell(row=r, column=col, value=-row["recurring_charge"]); c.number_format = MONEY0 c = ws.cell(row=r, column=col + 1, value=row["net"]); c.number_format = MONEY0 col += 2 if spec["interest"]: c = ws.cell(row=r, column=col, value=-row["interest"]); c.number_format = MONEY0 c = ws.cell(row=r, column=col + 1, value=row["balance"]); c.number_format = MONEY0 col += 2 if cum_net_col: c = ws.cell(row=r, column=cum_net_col, value=row["cum_net"]) c.number_format = MONEY0 c.font = Font(bold=True) if idx % 2 == 1: for cc in range(1, len(headers) + 1): if not ws.cell(row=r, column=cc).fill.fgColor.rgb or \ ws.cell(row=r, column=cc).fill.fgColor.rgb == "00000000": ws.cell(row=r, column=cc).fill = PatternFill("solid", fgColor=LIGHT) if cum_net_col: for key in ["cogs", "sale", "msrp"]: if key not in spec["milestones"]: continue hit = model["crossings"][key] if hit: cell = ws.cell(row=first + hit - 1, column=cum_net_col) cell.fill, cell.border = MILESTONE_FILL[key], BOX for i, w in enumerate([8, 10, 9, 17], start=1): ws.column_dimensions[get_column_letter(i)].width = w for i in range(5, len(headers) + 1): ws.column_dimensions[get_column_letter(i)].width = 15 ws.freeze_panes = ws.cell(row=first, column=3) return ws # -------------------------------------------------------------------------- # Cost basis sheet (2A only) # -------------------------------------------------------------------------- def write_cost_basis(wb, model): ws = wb.create_sheet("Cost Basis") ws.sheet_view.showGridLines = False ws["A1"] = "Odoo Line-Item Cost Basis" ws["A1"].font = TITLE_FONT ws["A2"] = f"Source: {model['cfg'].get('sale_order','')}" ws["A2"].font = Font(italic=True, size=10) headers = ["Line Item", "Qty", "MSRP / Unit", "Extended MSRP", "Disc %", "Net Subtotal", "Margin", "COGS", "Type"] hdr_row = 4 for i, h in enumerate(headers, start=1): _hdr(ws, hdr_row, i, h) r = hdr_row + 1 for gname, group in [("One-Time (financed capital)", model["one_time"]), ("Recurring (annual, ongoing)", model["recurring"])]: if not group: continue _band(ws, r, gname, len(headers)) r += 1 for l in group: cogs = l["net_subtotal"] - l["margin"] ws.cell(row=r, column=1, value=l["name"]) ws.cell(row=r, column=2, value=l["qty"]) for col, val, fmt in [(3, l["msrp_unit"], MONEY), (4, l["msrp_unit"] * l["qty"], MONEY), (5, l.get("discount", 0) / 100.0, PCT), (6, l["net_subtotal"], MONEY), (7, l["margin"], MONEY), (8, cogs, MONEY)]: cc = ws.cell(row=r, column=col, value=val); cc.number_format = fmt ws.cell(row=r, column=9, value="Recurring/yr" if l.get("recurring") else "One-time") if l["margin"] == 0 and l["net_subtotal"] > 0: ws.cell(row=r, column=7).font = Font(color="B00000", bold=True) elif abs(l["margin"] - l["net_subtotal"]) < 1e-6 and l["net_subtotal"] > 0: ws.cell(row=r, column=8).font = Font(color="B00000", bold=True) ws.cell(row=r, column=9, value="One-time — no cost set, verify") r += 1 msrp, net, cogs = line_totals(group) ws.cell(row=r, column=1, value=f"Subtotal — {gname}").font = LABEL_FONT for col, val in [(4, msrp), (6, net), (8, cogs)]: cc = ws.cell(row=r, column=col, value=val) cc.number_format, cc.font = MONEY, LABEL_FONT cc.border = Border(top=THIN, bottom=Side(style="double")) r += 2 t = model["totals"] ws.cell(row=r, column=1, value="TOTAL CONTRACT (yr 1 as quoted)").font = Font(bold=True, size=11) for col, val in [(4, t["contract_msrp"]), (6, t["contract_net"]), (8, t["contract_cogs"])]: cc = ws.cell(row=r, column=col, value=val) cc.number_format, cc.font, cc.fill = MONEY, Font(bold=True, size=11), SUB_FILL r += 2 mr = model["monthly_recurring"] _note(ws, r, f"Recurring cost carried monthly against {model['recurring_bearer']}: " f"${mr:,.2f}/mo (${mr * 12:,.2f}/yr, ${mr * model['horizon']:,.2f} over " f"{model['horizon']} months). Red COGS figures are lines with no standard " f"cost in Odoo — the true COGS target is higher than shown.", 9, 30) ws.column_dimensions["A"].width = 62 for col in "BCDEFGH": ws.column_dimensions[col].width = 15 ws.column_dimensions["I"].width = 30 return ws # -------------------------------------------------------------------------- # Assumptions sheet # -------------------------------------------------------------------------- def write_assumptions(wb, model): cfg, spec = model["cfg"], model["spec"] ws = wb.create_sheet("Assumptions") ws.sheet_view.showGridLines = False ws["A1"] = f"Model {spec['key']} — Assumptions" ws["A1"].font = TITLE_FONT ws.column_dimensions["A"].width = 44 ws.column_dimensions["B"].width = 22 ws.column_dimensions["C"].width = 54 r = 3 ad = cfg["ad_revenue"] def kv(label, value, note="", fmt=None): nonlocal r ws.cell(row=r, column=1, value=label).font = LABEL_FONT c = ws.cell(row=r, column=2, value=value) if fmt: c.number_format = fmt ws.cell(row=r, column=3, value=note).font = Font(size=9, color="666666") r += 1 _band(ws, r, "Advertising Revenue (per proposal)", 3); r += 1 for k, label, fmt in [("screen_count", "Screens in proposal", "#,##0"), ("monthly_onboardings", "Monthly on-boardings", "#,##0"), ("journey_minutes", "Avg journey time (min)", "0.0"), ("ad_slots", "15-second ad slots", "#,##0"), ("monthly_impressions", "Total monthly impressions", "#,##0"), ("fill_rate", "Assumed blended fill rate", PCT)]: if k in ad: kv(label, ad[k], "", fmt) kv("Gross monthly ad revenue", ad["monthly_gross"], "Steady-state total", MONEY) kv("Gross annual ad revenue", ad.get("annual_gross", ad["monthly_gross"] * 12), "", MONEY) r += 1 if ad.get("sources"): _band(ws, r, "Revenue Sources", 3); r += 1 for s in ad["sources"]: ws.cell(row=r, column=1, value=s["name"]) c = ws.cell(row=r, column=2, value=s["monthly"]); c.number_format = MONEY bits = [] if s.get("cpm"): bits.append(f"CPM ${s['cpm']:.2f}") if s.get("fill"): bits.append(f"{s['fill']:.0%} fill") ws.cell(row=r, column=3, value=", ".join(bits)).font = Font(size=9, color="666666") r += 1 r += 1 _band(ws, r, "Revenue Split", 3); r += 1 for p, s in spec["shares"].items(): kv(p, s, "", PCT) r += 1 _band(ws, r, "Model Parameters", 3); r += 1 kv("Horizon", f"{model['horizon']} months") kv("Ramp length", f"{cfg.get('ramp_months', 12)} months", "Linear ramp reflecting the proposal's fill-rate caveat") kv("Month-1 revenue", cfg.get("ramp_start_pct", 0.25), "Fraction of steady-state at launch", PCT) fin = model["finance"] if fin["active"]: kv("Capital financed by", "MPM", "Principal is MPM's one-time COGS outlay") kv("Principal", fin["principal"], "", MONEY) kv("Annual cost of money", fin["rate"], "Interest accrues monthly on the declining balance", PCT) kv("Debt service", "MPM share, interest first", "Milestones clear only after capital and accrued interest are recovered") else: kv("Capital financed by", "Customer" if spec["key"] == "2C" else "MPM", "No interest modeled — " + ("the customer purchased the system outright" if spec["key"] == "2C" else "this variant is revenue-only")) kv("Recurring licensing borne by", model["recurring_bearer"], f"${model['monthly_recurring']:,.2f}/mo at " f"{'vendor cost' if model['recurring_bearer'] == 'MPM' else 'invoiced price'}") kv("Client contribution", sum(c["amount"] for c in cfg.get("client_contributions", [])) or 0, "Deposits or milestone payments offsetting financed capital", MONEY) r += 1 _band(ws, r, "Exclusions and Caveats", 3); r += 1 notes = list(cfg.get("notes", [])) for s in cfg.get("surges", []): if not s.get("enabled"): notes.append(f"{s.get('label', 'Event surge')} excluded from base model " f"(${s['total']:,.0f} over {s['months']} months) — treated as upside.") notes.append("Ad revenue is a forecast, not a contracted receivable. Fill rates may be " "materially lower until repeat advertisers and programmatic demand establish.") if spec["milestones"]: notes.append("Payoff targets represent the one-time capital at risk. Recurring " "licensing runs as an ongoing monthly charge for the full horizon.") if spec["key"] == "2C": notes.append("The End User purchased the system, so the MSRP milestone measures the " "customer's own payback from their 70% share — not MPM's return.") notes.append("This model pairs one advertising proposal with one sales order. Its revenue " "figures are not transferable to any other deployment.") for n in notes: ws.cell(row=r, column=1, value="• " + n).font = Font(size=9) ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=3) ws.cell(row=r, column=1).alignment = Alignment(wrap_text=True, vertical="top") ws.row_dimensions[r].height = 28 r += 1 return ws # -------------------------------------------------------------------------- def build_workbook(cfg, model_key, path): model = build_model(cfg, model_key) wb = Workbook() wb.remove(wb.active) write_summary(wb, model) write_projection(wb, model) if model["spec"]["show_cost_basis"]: write_cost_basis(wb, model) write_assumptions(wb, model) wb.save(path) return model def recap(model, path): spec, fin = model["spec"], model["finance"] last = model["rows"][-1] print(f"\n{os.path.basename(path)} — Model {spec['key']}: {spec['title']}") print(" split: " + ", ".join(f"{p} {s:.1%}" for p, s in spec["shares"].items())) print(f" {model['horizon']}-mo distribution: " + ", ".join(f"{p} ${last['cum_parties'][p]:,.0f}" for p in model["parties"])) if fin["active"]: print(f" financed ${fin['principal']:,.2f} @ {fin['rate']:.1%} → " f"interest ${fin['total_interest']:,.2f}") if fin["neg_amort_months"]: print(f" !! negative amortization months " f"{fin['neg_amort_months'][0]}–{fin['neg_amort_months'][-1]}") for key in spec["milestones"]: hit = model["crossings"][key] print(f" {MILESTONE_LABEL[key]:<10} ${model['targets'][key]:>11,.2f} → " f"{'month ' + str(hit) if hit else 'not reached'}") if not spec["milestones"]: print(" (revenue distribution only — no cost basis or milestones)") def main(): ap = argparse.ArgumentParser() ap.add_argument("config") ap.add_argument("--model", "-m", default="all", choices=["2A", "2B", "2C", "all"]) ap.add_argument("-o", "--output", help="Output path (single model only)") ap.add_argument("--outdir", "-d", default=".", help="Directory for --model all") ap.add_argument("--prefix", default=None, help="Filename prefix; defaults to the sales order") args = ap.parse_args() with open(args.config) as f: cfg = json.load(f) keys = ["2A", "2B", "2C"] if args.model == "all" else [args.model] prefix = args.prefix or cfg.get("sale_order", "cap_table") slug = {"2A": "2A_Internal_MPM_Financed_ROI", "2B": "2B_Internal_MPM_Financed_Revenue", "2C": "2C_Customer_Financed_Acquisition"} p = None for key in keys: if args.output and len(keys) == 1: path = args.output else: os.makedirs(args.outdir, exist_ok=True) path = os.path.join(args.outdir, f"{prefix}_{slug[key]}.xlsx") model = build_workbook(cfg, key, path) recap(model, path) p = model["pairing"] if p and p["proposal"] and p["quote"] and not p["ok"]: print(f"\n!! SCREEN COUNT MISMATCH: proposal {p['proposal']:g} vs quote " f"{p['quote']:g} — verify these describe the same deployment") elif p and not (p["proposal"] and p["quote"]): print("\n.. screen count not reconciled (set ad_revenue.screen_count and " "is_display on the display line)") return 0 if __name__ == "__main__": sys.exit(main())