feat: add references/config_schema.md (v1.0.0)
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user