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

157 lines
6.0 KiB
Markdown
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.
# Config schema for build_cap_table.py
A single JSON file. Everything except `ad_revenue.monthly_gross` and `lines` has a default.
```json
{
"project_name": "SNA John Wayne Airport Shuttle — Onboard Infotainment",
"customer": "John Wayne Airport-Orange County",
"sale_order": "S01027",
"ad_proposal_ref": "Passent Media JWA001-071425 (14-Jul-2026)",
"prepared_on": "2026-09-11",
"start_date": "2027-03",
"horizon_months": 60,
"ramp_months": 12,
"ramp_start_pct": 0.25,
"finance": {
"annual_rate": 0.12,
"principal_basis": "cogs",
"method": "declining_balance"
},
"client_contributions": [
{"month": 1, "amount": 0}
],
"ad_revenue": {
"screen_count": 11,
"monthly_onboardings": 64000,
"journey_minutes": 6,
"ad_slots": 24,
"monthly_impressions": 1536000,
"fill_rate": 0.65,
"monthly_gross": 22138,
"annual_gross": 265651,
"sources": [
{"name": "TransitAds.co (6 local ads/mo)", "monthly": 12000, "fill": 0.25},
{"name": "Programmatic Exchange Min. (Ad Floor)", "monthly": 2765, "cpm": 12.00, "fill": 0.15},
{"name": "National Direct Buy", "monthly": 7373, "cpm": 24.00, "fill": 0.20}
]
},
"surges": [
{"label": "2028 LA Games", "start_month": 18, "months": 2.5,
"total": 334200, "enabled": false}
],
"notes": [
"Optional MP.TV Fully Managed Support is included as quoted."
],
"lines": [
{
"name": "M27V — Ruggedized, Intelligent Bus Display (Direct, G4)",
"qty": 11,
"msrp_unit": 4096.00,
"discount": 25.0,
"net_subtotal": 33792.00,
"margin": 9918.98,
"recurring": false,
"is_display": true
}
]
}
```
## Models and splits
The three model variants are presets in the script — you do not declare them in the config.
Run `--model all` and you get all three; the splits, milestones, interest behaviour and
sheet set come from the preset:
| Key | Split | Milestones | Party | Interest | Recurring borne by |
|---|---|---|---|---|---|
| 2A | MPM 30 / Passent 30 / Reseller 10 / End User 30 | COGS, sale, MSRP | MPM | yes | MPM, at vendor cost |
| 2B | same | none | — | no | MPM, at vendor cost |
| 2C | MPM 5 / Reseller 2.5 / Passent 22.5 / End User 70 | MSRP | End User | no | End User, at invoiced price |
Override a preset only when a specific deal was negotiated differently:
```json
"models": {
"2A": {"shares": {"MPM": 0.35, "Passent": 0.30, "Reseller": 0.10, "End User": 0.25}}
}
```
Shares must sum to exactly 1.0 — the script raises rather than building a split that loses
or duplicates a party's dollars.
## Financing
`finance.annual_rate` is the cost of money (default 0.12). `principal_basis` picks which
target the loan is drawn against — `cogs` (default, and the right answer: it is the cash
MPM actually spends), `sale`, or `msrp`. Set `principal_amount` instead to hard-code a
figure, e.g. when only part of the scope is financed.
Interest applies to 2A only. It accrues monthly on the declining balance; MPM's share
services interest first, then principal. Milestones clear when cumulative net covers the
target plus interest accrued to date, which makes the COGS milestone and the balance
reaching zero the same month. If those two disagree in the output, the config is wrong.
## The pairing fields
`sale_order` and `ad_proposal_ref` identify the one-to-one pair this model represents, and
both appear at the top of the Summary sheet. A workbook that does not name both is not
finished.
`ad_revenue.screen_count` and `is_display` drive an automatic reconciliation: the screen
count the proposal's impression math assumes should equal the display quantity on the
quote. Set `is_display: true` on the display/panel line (or lines) and the script compares
the two, printing a warning and flagging the Summary sheet in red on a mismatch. A mismatch
means the proposal and the quote describe different deployments — resolve it before
delivering the model rather than shipping a projection built on a foreign assumption.
## Field notes
**`lines`** — one object per Odoo `sale.order.line`, excluding `line_section` and
`line_note` rows. Map straight across:
| Config field | Odoo field |
|---|---|
| `name` | `name` (first line is enough; long descriptions clutter the sheet) |
| `qty` | `product_uom_qty` |
| `msrp_unit` | `price_unit` |
| `discount` | `discount` (percent, e.g. `25.0`) |
| `net_subtotal` | `price_subtotal` |
| `margin` | `margin` |
| `recurring` | your classification — see SKILL.md step 3 |
COGS is derived as `net_subtotal margin`, so there is no separate cost field to fill in.
**`recurring: true`** means the line's `net_subtotal` and cost represent **one year**.
The script divides it by 12 and charges it every month for the whole horizon, and excludes
recurring lines from the payoff targets. Which basis is charged depends on who bears it:
MPM pays vendor cost (`net_subtotal margin`), the End User pays the invoiced price
(`net_subtotal`). The preset decides the bearer per model.
**`start_date`** — the month the screens go live and ads start, not the contract date.
The quote's terms give a six-month standard delivery timeline, so this is usually the
quotation month plus six.
**`ramp_start_pct` / `ramp_months`** — month 1 runs at `ramp_start_pct` of steady-state
and climbs linearly to 100% at `ramp_months`. With the defaults (25%, 12 months) the ramp
costs roughly three months of revenue versus an instant-on assumption.
**`client_contributions`** — dated one-off credits against the financed balance. Use this
for deposits, milestone payments, or an agency capital contribution. Multiple entries in
the same month are summed.
**`surges`** — `total` is spread evenly across `months` starting at `start_month`. Leave
`enabled: false` for the base model; flip it on to produce an upside variant. A disabled
surge still appears in the Assumptions sheet as a documented exclusion, which is the point.
**`client_contributions`** credit only the milestone party (MPM in 2A, the End User in 2C),
since a deposit offsets the capital of whoever fronted it.