loopable / platform /core /registry.py
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
bf8519f verified
Raw
History Blame Contribute Delete
18.5 kB
"""Module registry β€” the single source of truth for the dashboards.
`app.py` builds the sidebar nav from this, and decides whether the global Brand (DBA) filter
applies to a page. `validate.py` runs each module's validate() from this. Adding a module =
add one row here + a page function in app.py + a module in modules/.
Fields:
key stable id (matches the modules/<key>.py file and the app page_<key> function)
label sidebar/title text (no emojis)
brand True -> DBA-filterable (honours the Fisch/Royal selector)
False -> consolidated/company-level (ignores the brand selector)
hq True -> HQ / company-level (cash flow, balance sheet, AR/AP, consolidated)
validate True -> module exposes validate() and is included in validate.py
parent set -> this row is a SUB-MODULE of <parent key>: it leaves the Workflows dropdown and
renders as a sub-nav button when its parent (or a sibling) is the active module; a
parent grant covers its sub-modules (owner nav redesign 2026-07-23)
archived True -> RETIRED from the UI for EVERYONE (owner 2026-07-27, supersedes the
2026-07-23 per-user-restore semantics): no nav entry, no Settings>Modules checkbox,
prewarm and validate.py skip it. Code and data layer stay in the repo but are NOT
maintained β€” spend no effort on archived modules until the owner un-archives.
2026-07-23 pass: backorders, pricecomp, o2c, bookings, spend, expenses.
2026-07-27 pass (keep = Sales + Customers family + Agents + Procurement +
Collections + Analyst/plumbing): products, assortment, financial, management,
pricing, inventory, vendor, warehouse, returns, outreach, complexity, health.
note one-line scope note shown in the UI / docs
"""
# WAVE-9 I8 β€” `'source'`: which connected system a module's data comes from.
#
# The owner's model: every nav entry is "a piece of database from a source", so the nav shows the
# source beside the name ("Sales Β· Odoo"). It is a REGISTRY FACT rather than a string in the nav
# code precisely so the nav needs no edit when a second connector lands β€” a user-created blank
# table simply carries a different source, and a module with no `source` (Metric Dictionary,
# Settings, the Analyst) renders no badge because it is not a database at all.
# See [[connector-onboarding]]: the plug-in seam is datastore.py, and Odoo has no delegated
# OAuth, so the "+ New" connect flow is the credential-form shape, not an OAuth redirect.
REGISTRY = [
# ARCHIVED wave 16 (owner item 7, R3/R11, 2026-08-02): the PAGE went on the strength of
# C-CHARTCAP β€” every Sales block shape (YoY compare series, KPI delta tiles, group-by
# tables) is now assemblable BY HAND from the grid's chart/dashboard views, which is what
# R3 required before deletion. `modules/sales.py` (the data layer) STAYS β€” drawers and the
# briefing still read it, and pages_sales.py survives UNREGISTERED as the Y1 envelope's
# template + verify_api's fixture.
{'key': 'sales', 'label': 'Sales', 'brand': True, 'hq': False, 'validate': True, 'source': 'Odoo', 'archived': True,
'note': 'Revenue, YoY, seasonality, reps, customers, SKUs β€” by BU. Retired wave 16: '
'rebuild as grid chart/dashboard views (compare series, KPI deltas, tables).'},
# ARCHIVED AS A PAGE (owner item 14, wave 8): "archive the current form of the Customer
# dashboard completely, i want to redesign it, exactly with the backend we have". So the
# PAGE goes and the DATA LAYER stays β€” modules/customers.py still powers the Customer table's
# metrics and still validates against Odoo, which is what the redesign will be built on.
#
# ⚠ NOT 'archived': True. Archived means invisible to everyone, and this row is the parent of
# customer_data + cohort β€” archiving it would take the whole family out of the nav and strand
# its children. 'group_only' says exactly what is true: this key names a FOLDER, never a
# destination. It has no PAGE_FUNCS entry, the nav never renders it as a leaf, and a stale
# deep link to it redirects to the family's first visible member.
{'key': 'customers', 'label': 'Customers', 'brand': True, 'hq': False, 'validate': True,
'group_only': True,
'note': 'Folder for the customer surfaces (Customer table, Cohort). The old Customers '
'dashboard was retired for redesign (owner item 14, 2026-07-28); its metric layer '
'(modules/customers.py) still backs the table and still validates against Odoo.'},
# LABEL renamed 'Customer List' -> 'Data' (owner 2026-07-25) -> 'Customer' (owner item 5,
# 2026-07-27); KEY renamed 'customer_list' -> 'customer_data' (owner 2026-07-26). The key
# rename is safe ONLY because _LEGACY_KEYS (ui/session.py) migrates grants, Library prefs
# and ?page= deep links ON READ β€” stored user records are never edited, exactly as
# myday->customer_list was done. The STORE keys are deliberately NOT renamed:
# 'customer_table_workspace' and 'customer_lists' hold real user data, and renaming them
# would orphan every saved view. Label-only changes need none of that machinery.
{'key': 'customer_data', 'label': 'Customer', 'brand': True, 'hq': False, 'validate': False, 'source': 'Odoo', 'parent': 'customers',
'note': 'Build, filter and save customer lists: the whole scoped book with per-customer '
'metrics; saved lists are visible, editable formulas (Call list and Win-back ship '
'as templates) plus hand-picked members. Sub-module of Customers (owner nav redesign 2026-07-23; replaced My Day).'},
# Cohort (owner item 6, 2026-07-26): the SAME table as Data over a FIXED set. The
# difference is membership semantics, not layout β€” a saved view re-populates as the
# data moves, a cohort only changes when a person edits it. validate:False because
# there is no Odoo aggregate to reconcile a hand-picked list against; its metrics are
# the customers module's, already validated there.
# ARCHIVED wave 16 (owner item 5, R10 β€” the fold-in): every cohort now projects as a
# LOCKED VIEW in the Customer rail's "Cohorts" section (wave-15 C-LOCK), so the separate
# nav destination is gone. ALL cohort machinery stays: `scope=cohort` still works
# (routes_grid._SCOPES, storage keys, events), page_cohort remains in app.py, and
# _LEGACY_KEYS maps cohort→customer_data so grants + ?page= deep links land on the grid
# that now hosts the cohorts.
{'key': 'cohort', 'label': 'Cohort', 'brand': True, 'hq': False, 'validate': False, 'source': 'Odoo', 'parent': 'customers', 'archived': True,
'note': 'Hand-curated, unchanging customer lists β€” folded into the Customer rail as '
'locked views (wave 16). The set is fixed: it changes only when someone adds '
'or removes a member. Open them from Customer > Views > Cohorts.'},
# ARCHIVED wave 16 (owner item 7, R11, 2026-08-02) beside Sales. The agent DRAWER and the
# data layer (modules/agent.py, agent_* caches) STAY β€” agent entities still open from
# customer surfaces; an own-book agent login's home is the Customer grid narrowed by the
# R1 permanent filter ("Agent is X"), not this page.
{'key': 'agent', 'label': 'Agents', 'brand': True, 'hq': False, 'validate': True, 'source': 'Odoo', 'archived': True,
'note': 'Per-agent book: sales (custom dates), returns, top SKUs, full customer list incl. inactive. '
'Retired wave 16: agent drawers + the R1 own-book filter carry the use case.'},
# Wave 16 C-TOPIC (owner items 9+10, R4): the PRODUCT table family β€” the second object on
# the table-page factory. `products_family` is the folder head (the `customers` pattern:
# group_only, never a destination); `product_data` is the grid over the SKU dataset, with
# its OWN workspace bucket ('product_table_workspace') and the /products + scope=product
# seams. The archived Streamlit `products` (SKU) row below stays archived and untouched,
# exactly as R4 rules.
{'key': 'products_family', 'label': 'Products', 'brand': True, 'hq': False, 'validate': False,
'group_only': True,
'note': 'Folder for the product surfaces (Product table). The archived SKU dashboard is '
'not part of this family.'},
{'key': 'product_data', 'label': 'Product', 'brand': True, 'hq': False, 'validate': True, 'source': 'Odoo', 'parent': 'products_family',
'note': 'The SKU catalogue as a grid: per-product revenue, units and (consolidated) '
'stock columns, with saved views and custom fields. Identity = the SKU code.'},
{'key': 'products', 'label': 'SKU', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'SKU movers, zombies, velocity, coverage, drawers β€” by BU (sales-derived).'},
{'key': 'assortment', 'label': 'Assortment', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Facet-level performance (category/color/occasion/collection/season) + season readiness β€” by BU.'},
{'key': 'financial', 'label': 'Financial', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Gross margin by BU/category/SKU. Cash-conversion cycle is HQ-consolidated.'},
{'key': 'pricing', 'label': 'Pricing', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Per-SKU margin/markup/LTM sales + allocated net P&L (channel-rate cost-to-SKU) for pricing decisions.'},
{'key': 'management', 'label': 'Management P&L', 'brand': False, 'hq': True, 'validate': True, 'archived': True,
'nav': False, # rendered INSIDE the Financial module (view toggle) β€” archived WITH financial
'note': 'Operating-model management income statements (FFS/RI/GD/BU-M) reproduced from raw Odoo, Sep-2024+.'},
{'key': 'inventory', 'label': 'Inventory', 'brand': False, 'hq': False, 'validate': True, 'archived': True,
'note': 'On-hand stock is physically consolidated (shared warehouse); not DBA-split.'},
{'key': 'procurement', 'label': 'Procurement', 'brand': False, 'hq': False, 'validate': True, 'source': 'Odoo',
'validate_only': True,
'note': 'PAGE RETIRED 2026-08-03 (owner wave-17 item 13: "We should be able to replace '
'Procurement completely, and add it as part of the Product database"). The buy '
'list is now a saved VIEW on the Product grid, filtered on a FORMULA field over '
'the supplier/lead-time columns and the demand measure β€” the owner\'s ruling R3: '
'"Buy list is just a View, with a Filter from a Formula field that taps into '
'Metrics Fields... the 8-month demand baseline etc. is just math in a Formula '
'field." The ROW STAYS validate_only: `modules/procurement.py` still owns the '
'supplier master map (procurement_suppliers.json, 3,963 SKUs) that seeds those '
'columns, and its validate() is the proof they reconcile.'},
{'key': 'vendor', 'label': 'Vendors', 'brand': False, 'hq': False, 'validate': False, 'archived': True,
'note': 'Vendor master data + the SKUs each supplies; multiple-vendor / cheapest-price view.'},
{'key': 'warehouse', 'label': 'Warehouse', 'brand': False, 'hq': False, 'validate': True, 'archived': True,
'note': 'Movement & efficiency: on-time ship, Pick-Pack-Ship cycle times, throughput, backlog age, shrinkage. Physical ops (consolidated).'},
{'key': 'ar', 'label': 'Collections', 'brand': False, 'hq': True, 'validate': True,
'source': 'Odoo', 'api_surface': False, 'admin_only': True,
'note': 'PAGE RETIRED 2026-08-03 (owner wave-17 item 15: "Collections should also be '
'entirely replicable as just a View under Customer"). The worklist is the shared '
'"Collections" view on the Customer grid β€” same numbers, from this module\'s own '
'reconciled blocks (ar_open/ar_overdue/ar_exposure/days_to_pay + the four aging '
'buckets). The ROW STAYS validate_only so validate.py keeps running ar.validate(), '
'which is the proof those columns rest on; archiving it would have skipped the '
'reconciliation while the numbers kept shipping. ⚠ NOT `validate_only`, and the '
'difference is a LIVE WORKFLOW: this page also mounts the admin-gated STATEMENTS '
'sender (app._collections_statements -> modules/collections_send), the ONE '
'sanctioned Odoo writer, which the same ruling says stays untouched. So the row '
'keeps a Streamlit page for ADMINS ONLY (`admin_only`) and leaves the API payload '
'entirely (`api_surface: False`) β€” no "Collections" in the React nav, no second '
'worklist, and the biweekly send keeps its door.'},
{'key': 'returns', 'label': 'Returns', 'brand': False, 'hq': True, 'validate': True, 'archived': True,
'note': 'Credit-note lens: refund concentration by SKU (quality) and customer (behavior). Company-level.'},
{'key': 'outreach', 'label': 'Outreach', 'brand': True, 'hq': False, 'validate': False, 'archived': True,
'note': 'Campaign emails to customer segments: templates, suppression, send log, revenue attribution. Sending is admin-gated behind SAFE_MODE.'},
{'key': 'backorders', 'label': 'Backorders', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Confirmed-undelivered order lines aged vs promise date, valued, with a supply-aware next action per row (ship / expedite / call). Wholesale scope.'},
{'key': 'pricecomp', 'label': 'Price Compliance', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Selling below the customer pricelist tier (LTM, per customer x SKU): the pocket-price floor worklist with annualized leak $. Sub-30% ratios usually mean a stale or pack-basis RULE β€” fix the rule, not the rep.'},
{'key': 'o2c', 'label': 'Cash Timing', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Order-to-cash stage decomposition (order-to-ship / ship-to-invoice / invoice-to-paid, each with its owner) + the terms-gap rollup: contractual vs actual days per payment term with the free-credit $ it strands.'},
{'key': 'bookings', 'label': 'Order Book', 'brand': True, 'hq': False, 'validate': True, 'archived': True,
'note': 'Pre-season booking coverage: cumulative booked $ for Aug-Dec delivery vs last year same-week; category TY-vs-LY-as-of-date. The container-program decision, months ahead.'},
{'key': 'spend', 'label': 'Spend & Payables', 'brand': False, 'hq': True, 'validate': True, 'archived': True,
'note': 'Vendor-bill side: payment-terms capture (early-pay cash surrendered), duplicate-bill review, the opex spend cube (fragmentation, consolidate+rebid) and freight recovery. Company-level.'},
{'key': 'complexity', 'label': 'SKU Complexity', 'brand': False, 'hq': False, 'validate': True, 'archived': True,
'note': 'BCG tail rationalization: every SKU re-costed by ACTIVITY (lines, picks, returns) + 25%/yr carrying; KILL candidates rest on hard math (GM minus carrying), REVIEW on the pooled activity estimate. All-channel.'},
{'key': 'expenses', 'label': 'Expenses', 'brand': False, 'hq': True, 'validate': True, 'archived': True,
'note': 'Operating expense from the GL (all expense-type accounts; COGS excluded): trend, operating leverage (opex % of revenue), the YoY cost bridge, XmR control-limit spike watch list, fixed/variable split and a drill-to-ledger category directory. Company-level.'},
{'key': 'health', 'label': 'Data Health', 'brand': False, 'hq': True, 'validate': True, 'archived': True,
'note': 'Close/reconciliation scan; period-filtered, mostly company-level. ARCHITECTURE '
'(owner 2026-07-23): every discrepancy / potential-error marker lives here β€” '
'procurement mapping gaps, count-trust (unverified on-hand counts), untracked-on-'
'order β€” so operational workflows stay clean for doing the work.'},
{'key': 'dictionary', 'label': 'Metric Dictionary', 'brand': False, 'hq': True, 'validate': True,
'nav': False, 'validate_only': True,
'note': 'PAGE RETIRED 2026-07-23 (owner: broken/not customer-facing) β€” row kept ONLY so '
'validate.py keeps running the semantic-layer contracts (the Analyst grounding '
'proof). No nav, no page. Wave 17 R8 ("I don\'t even know what it does β€” delete '
'them") made that literal: `validate_only` takes it out of the account menu too, '
'which is the last place it was still visible. The proof survives; the door does not.'},
{'key': 'automation', 'label': 'Automation', 'brand': False, 'hq': True, 'validate': False,
'nav': True,
'note': 'Wave 18 (C-AUTONAV): the Automation surface β€” scheduled jobs that create and '
'refresh user databases (website scrape-to-DB, the Instagram field). React-only '
'surface (no PAGE_FUNCS entry, the no-new-Streamlit rule); admins hold it via '
'"all", other users need the explicit grant β€” fail-closed default.'},
{'key': 'settings', 'label': 'Settings', 'brand': False, 'hq': True, 'validate': False,
'nav': False, # sidebar sentinel (account group) β€” owner IA 2026-07-12
'note': 'User scope settings: the Business Unit toggle (strict isolation) and the Data basis '
'toggle (Orders vs Invoiced) moved here from the sidebar.'},
{'key': 'analyst', 'label': 'AIOS Analyst', 'brand': False, 'hq': True, 'validate': False,
'nav': False, # sidebar sentinel button; eval gate = harness/evals.py (run pre-ship, NOT in validate.py β€” live LLM cost)
'note': 'Ask the business a question in plain language: a small AI model calls governed tools over the semantic layer β€” answers carry their query trace and drill links. AI-generated output (Art. 50 labeled).'},
]
BY_KEY = {m['key']: m for m in REGISTRY}
def brand_filterable(key) -> bool:
return bool(BY_KEY.get(key, {}).get('brand'))