diff --git a/references/config_schema.md b/references/config_schema.md new file mode 100644 index 0000000..ef4035c --- /dev/null +++ b/references/config_schema.md @@ -0,0 +1,156 @@ +# 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.