Files
2026-09-11 14:24:02 -05:00

843 lines
36 KiB
Python
Raw Permalink 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
"""
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())