loopable / platform /aios_grid_fields.json
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
e1b3e71 verified
Raw
History Blame Contribute Delete
19.7 kB
{
"_comment": "CANONICAL field contract for the AIOS Airtable-style grid — the SINGLE source of truth. Consumed by platform/aios_grid.py (embed/Space host) and aios-web/api/main.py (standalone API), and regenerated into aios-web/web/public/sample_customers.json. Edit HERE only, then run aios-web/verify_fields_contract.py. source=odoo is READ-ONLY; source=overlay is the editable stratum (notes/tags) outside Odoo. type in {text,status,select,currency,int,date,pct} (select = a fixed-choice READ-ONLY brand attribute; dba is the first, wave 2026-08-02). `description` (wave 5) is the CANONICAL per-field description — every field must carry one, and since wave 7 (owner W8, 2026-07-28) every description is ONE SHORT PLAIN sentence (two only when a fact would otherwise mislead): what the field IS, nothing else — no filter tips, no '(none)' coaching, no rationale; the user's workspace NOTE overrides it in the (i) hover, never in this file. BUILDER FACT (documented here, deliberately NOT in user-facing text): blank text attributes display as '(none)', so `is '(none)'` — not `is empty` — finds the blanks on agent/city/state/country/zip/payment_terms/pricelist/tags. filterable:false = the CONDITION BUILDER does not offer it (still displayed, still sortable); every such field must have a replacement declared in aios-web/verify_fields_contract.py. 2026-07-27 partner attributes: country/zip/payment_terms/pricelist/tags/customer_since all ship default:false. zip is TEXT because a postal code has leading zeros. Odoo's credit_limit (1% populated) and user_id salesperson (2%) are deliberately ABSENT; agent_ids is the salesperson field and AR is where credit exposure comes from. Wave-5 item 8 (2026-07-27): ltm_rev and at_risk are DELETED — LTM's replacement is a creatable Sales measure column (the demo column IS Sales · the last 12 months), at_risk's replacement is a formula field, e.g. MAX(0, {revenue_ly} - {revenue_ytd}). Wave-6 item 8 (2026-07-27, the no-buildable-presets rule): revenue_ytd, revenue_ly, orders_24m, aov and yoy_pct are DELETED — every one is self-buildable, so a frozen pre-set beside the builder was two ways to ask one question. Replacements (recorded in verify_fields_contract.py): creatable measure columns for Sales / Orders / Avg order $ over any period (harness/measure_filter.py ADMITTED carries revenue, orders and the composite aov), and a formula over two measure columns for YoY, e.g. ({sales_ytd} - {sales_ly}) / {sales_ly}. Stale view colIds naming the five self-heal on the next autosave (the established rule).",
"fields": [
{
"key": "customer",
"label": "Customer",
"type": "text",
"source": "odoo",
"pinned": true,
"default": true,
"description": "The customer's name in Odoo. One row per customer who ordered in the last 24 months."
},
{
"key": "odoo_status",
"label": "Odoo record",
"type": "status",
"source": "odoo",
"default": false,
"description": "Whether this customer still exists in Odoo. Archived means deleted there."
},
{
"key": "agent",
"label": "Agent",
"type": "text",
"source": "odoo",
"default": true,
"description": "The sales agent who owns this account."
},
{
"key": "dba",
"label": "DBA",
"type": "select",
"source": "odoo",
"default": false,
"options": [
"Fisch",
"Royal",
"Both"
],
"description": "The brand this customer buys from - Fisch, Royal, or both. Amazon-channel orders are not a DBA."
},
{
"key": "salesperson",
"label": "Salesperson",
"type": "text",
"source": "odoo",
"default": false,
"description": "Who keyed in most of this customer's orders — not the Agent, who owns the account."
},
{
"key": "street",
"label": "Street",
"type": "text",
"source": "odoo",
"default": false,
"description": "First address line, from res.partner directly - not the geocoder, so a customer the map cannot place still shows its address."
},
{
"key": "street2",
"label": "Street 2",
"type": "text",
"source": "odoo",
"default": false,
"description": "Second address line (suite, unit, floor) on the customer's Odoo address."
},
{
"key": "city",
"label": "City",
"type": "text",
"source": "odoo",
"default": true,
"description": "City on the customer's Odoo address."
},
{
"key": "state",
"label": "State",
"type": "text",
"source": "odoo",
"default": true,
"description": "State or province on the customer's Odoo address."
},
{
"key": "country",
"label": "Country",
"type": "text",
"source": "odoo",
"default": false,
"description": "Country on the customer's Odoo address."
},
{
"key": "zip",
"label": "ZIP",
"type": "text",
"source": "odoo",
"default": false,
"description": "Postal code on the customer's Odoo address."
},
{
"key": "customer_since",
"label": "Customer since",
"type": "date",
"source": "odoo",
"default": false,
"description": "When this customer was first set up in Odoo."
},
{
"key": "tags",
"label": "Tags",
"type": "text",
"source": "odoo",
"default": false,
"description": "Odoo labels on this customer, comma-separated."
},
{
"key": "pricelist",
"label": "Price list",
"type": "text",
"source": "odoo",
"default": false,
"description": "The price list this customer buys on."
},
{
"key": "payment_terms",
"label": "Payment terms",
"type": "text",
"source": "odoo",
"default": false,
"description": "Payment terms on this customer's account — Net 30, for example."
},
{
"key": "last_order",
"label": "Last order",
"type": "date",
"source": "odoo",
"default": true,
"description": "Date of the most recent confirmed order."
},
{
"key": "overdue_days",
"label": "Overdue days",
"type": "int",
"source": "odoo",
"default": true,
"description": "How many days late this customer is running against their own usual ordering rhythm."
},
{
"_note": "filterable:false — DERIVED ANALYTIC: est_missed is min(cycles missed, 3) x AOV, a score we compute rather than an object the business has, so a condition on it would read as a fact about the customer when it is a fact about our arithmetic. It still displays and still sorts. Until wave 6 this flag also covered the frozen-window presets (revenue_ytd / revenue_ly / orders_24m / aov / yoy_pct); those are now DELETED outright under the owner's no-buildable-presets rule — see _comment. est_missed itself STAYS: no creatable measure or formula reproduces the cadence model behind it.",
"key": "est_missed",
"label": "Est. missed $",
"type": "currency",
"source": "odoo",
"default": true,
"agg": "sum",
"filterable": false,
"description": "Estimated sales missed while quiet: missed orders (capped at 3) times average order value. An estimate, not money owed."
},
{
"_note": "wave 21 R1 — KEY UNCHANGED, LABEL RENAMED. The computation is a DISJOINT split (ar.py credit_exposure): this column is only the not-yet-due residual, its sibling is the past-grace residual, and the two sum to the total. Under the label 'AR open $' the majority-late book read as 'Overdue > Open', which is nonsense in AR vocabulary — 'open' universally means the total. The label now says what the number is; the key stays so saved views and filters keep working.",
"key": "ar_open",
"label": "AR current $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Invoiced money owed but not yet due (a 5-day grace applies before it counts as overdue)."
},
{
"key": "ar_overdue",
"label": "AR overdue $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Invoiced money past due — same basis as the Collections page."
},
{
"_note": "wave 21 R1 — the TOTAL, added beside the rename above. AR current $ + AR overdue $, i.e. what most people mean by 'open AR'. Composed from the same ar.credit_exposure rows the siblings use, so it is transitively reconciled by ar.validate()'s residual read_group tie — no second oracle.",
"key": "ar_outstanding",
"label": "AR outstanding $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Total invoiced money owed right now: AR current $ plus AR overdue $."
},
{
"key": "ar_exposure",
"label": "Credit exposure $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "The most you could be out if they stopped paying today: open, overdue, draft and not-yet-invoiced."
},
{
"key": "ar_aged_1_30",
"label": "1-30 days $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Overdue between 1 and 30 days. The four aging buckets sum to AR overdue $."
},
{
"key": "ar_aged_31_60",
"label": "31-60 days $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Overdue between 31 and 60 days. The four aging buckets sum to AR overdue $."
},
{
"key": "ar_aged_61_90",
"label": "61-90 days $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Overdue between 61 and 90 days. The four aging buckets sum to AR overdue $."
},
{
"key": "ar_aged_90_plus",
"label": "90+ days $",
"type": "currency",
"source": "odoo",
"default": false,
"description": "Overdue by more than 90 days. The four aging buckets sum to AR overdue $."
},
{
"key": "days_to_pay",
"label": "Days to pay",
"type": "int",
"source": "odoo",
"default": false,
"description": "Average days to pay an invoice in full. Blank means no fully paid invoice yet."
},
{
"key": "top_category",
"label": "Top category",
"type": "text",
"source": "odoo",
"default": false,
"description": "The category this customer spent the most on in the last 12 months."
},
{
"key": "top_category_pct",
"label": "Top category %",
"type": "pct",
"source": "odoo",
"default": false,
"description": "Share of last-12-months spend that went to the top category."
},
{
"key": "sku_count",
"label": "SKUs bought",
"type": "int",
"source": "odoo",
"default": false,
"description": "Distinct products bought in the last 12 months."
},
{
"key": "top_sku",
"label": "Top SKU",
"type": "text",
"source": "odoo",
"default": false,
"description": "The product this customer spent the most on in the last 12 months."
},
{
"key": "days_since",
"label": "Days since order",
"type": "int",
"source": "odoo",
"default": false,
"description": "Days since the last confirmed order."
},
{
"key": "typical_gap_days",
"label": "Typical gap days",
"type": "int",
"source": "odoo",
"default": false,
"description": "Days this customer usually goes between orders, from their own history."
},
{
"key": "notes",
"label": "Notes",
"type": "text",
"source": "overlay",
"default": false,
"description": "Your notes on this customer. Saved in this app only, visible only to you."
}
],
"_product_comment": "ADDITIVE, wave 15 C-TOPIC. The PRODUCT table's field contract. Kept as a SEPARATE top-level key rather than restructuring `fields` into {customer_data, product_data}: both existing readers (aios_grid._load_fields, aios-web/api/main.py) index doc['fields'] directly, and reshaping that mid-wave would break the embed for a cosmetic gain. The keyed shape can arrive when both readers move in ONE commit; until then this is the product half and `fields` is the customer half.",
"_product_removed_buy_now": "OWNER, 2026-08-03: 'Buy signal' (key buy_now, a select of Buy now / OK) is NO LONGER A PRESET FIELD. It never earned one: it is a formula over two columns that are both still right here, and the platform has a formula field type for exactly that. THE FORMULA, which reproduces the retired column row for row (modules/product_data.validate proves the equivalence, and goes red if it ever stops holding): IF({lead_days} > 0, IF({dos} < {lead_days}, \"Buy now\", \"OK\"), \"\") . Every branch matches the old server rule, including the blanks - the formula engine refuses a comparison against a blank rather than coercing it to 0, so a SKU with no days-of-supply or no lead time comes out empty, which is 'we do not know' and not 'you are fine'. NOTE the column is still COMPUTED in product_data.pool(): it ships nowhere (rows_from_pool projects strictly through this contract, so no Field means no cell on the wire) and exists only as validate()'s oracle. A formula field is PER-USER, so nothing shared may filter on it - the Buy list view filters on dos/lead_days directly (_seed_wave17).",
"product_data": {
"identity": "pid",
"business_key": "code",
"fields": [
{
"key": "code",
"label": "SKU",
"type": "text",
"source": "odoo",
"pinned": true,
"default": true,
"description": "The SKU code — the product's real business key. `pid` is a stable CRC32 of it because the grid keys on an integer."
},
{
"key": "product",
"label": "Product",
"type": "text",
"source": "odoo",
"default": true,
"description": "Product name as it appears in Odoo."
},
{
"key": "category",
"label": "Category",
"type": "select",
"source": "odoo",
"default": true,
"description": "Product category; '(uncategorized)' when Odoo carries none."
},
{
"key": "supplier",
"label": "Supplier",
"type": "text",
"source": "overlay",
"default": true,
"description": "Who makes it. Editable here and shared with everyone in the workspace; seeded from the inventory mastersheet.",
"shared": true
},
{
"key": "origin_country",
"label": "Country",
"type": "text",
"source": "overlay",
"default": false,
"description": "Country of origin. Editable here and shared with everyone; seeded from the inventory mastersheet.",
"shared": true
},
{
"key": "lead_days",
"label": "Lead time (days)",
"type": "int",
"source": "overlay",
"default": true,
"description": "Order-to-arrival days for this supplier. Drives the buy signal. Editable and shared with everyone.",
"shared": true
},
{
"key": "first_cost",
"label": "First cost",
"type": "currency",
"source": "overlay",
"default": false,
"description": "Quoted unit cost at origin, before freight and duty. Editable and shared with everyone.",
"shared": true
},
{
"key": "price_fisch",
"label": "Fisch price",
"type": "currency",
"source": "odoo",
"description": "Fisch pricelist price for this SKU. Blank when that list prices it nowhere."
},
{
"key": "price_royal_1",
"label": "Royal 1 price",
"type": "currency",
"source": "odoo",
"description": "Royal 1 pricelist price for this SKU. Blank when that list prices it nowhere."
},
{
"key": "price_royal_2",
"label": "Royal 2 price",
"type": "currency",
"source": "odoo",
"description": "Royal 2 pricelist price for this SKU. Blank when that list prices it nowhere."
},
{
"key": "rev_ytd",
"label": "Revenue YTD",
"type": "currency",
"source": "odoo",
"default": true,
"description": "Year-to-date revenue for this SKU, BU-scoped when the caller is."
},
{
"key": "rev_ly",
"label": "Revenue LY",
"type": "currency",
"source": "odoo",
"description": "Same period last year — seasonal wholesale compares like for like."
},
{
"key": "yoy_pct",
"label": "YoY %",
"type": "pct",
"source": "odoo",
"description": "Year-over-year change; null when last year was zero (a ratio to zero is not a number)."
},
{
"key": "qty_ytd",
"label": "Units YTD",
"type": "int",
"source": "odoo",
"description": "Units sold year to date."
},
{
"key": "orders_ytd",
"label": "Orders YTD",
"type": "int",
"source": "odoo",
"description": "Distinct orders containing this SKU, year to date."
},
{
"key": "on_hand",
"label": "On hand",
"type": "int",
"source": "odoo",
"description": "Units in stock. CONSOLIDATED — one physical warehouse, not brand-tagged, so this column is ABSENT for a BU-scoped caller rather than silently company-wide."
},
{
"key": "unit_cost",
"label": "Unit cost",
"type": "currency",
"source": "odoo",
"description": "Inventory unit cost. Consolidated; absent for a BU-scoped caller."
},
{
"key": "inv_value",
"label": "Stock value",
"type": "currency",
"source": "odoo",
"description": "On-hand value at cost. Consolidated; absent for a BU-scoped caller."
},
{
"key": "qty_ltm",
"label": "Units LTM",
"type": "int",
"source": "odoo",
"description": "Units sold in the last twelve months. Consolidated; absent for a BU-scoped caller."
},
{
"key": "dos",
"label": "Days of supply",
"type": "int",
"source": "odoo",
"description": "Days of supply at the LTM rate; null means it never sells through. Consolidated; absent for a BU-scoped caller."
},
{
"key": "cover_gap_d",
"label": "Cover gap (days)",
"type": "int",
"source": "odoo",
"default": false,
"description": "Days of supply minus lead time. Negative means it runs out before a reorder lands."
},
{
"key": "stock_bucket",
"label": "Stock status",
"type": "select",
"source": "odoo",
"description": "Dead / excess / healthy bucket from the inventory module. Consolidated; absent for a BU-scoped caller."
}
]
}
}