| """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 |
| """ |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| REGISTRY = [ |
| |
| |
| |
| |
| |
| |
| {'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).'}, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| {'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.'}, |
| |
| |
| |
| |
| |
| |
| |
| {'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).'}, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| {'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.'}, |
| |
| |
| |
| |
| {'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.'}, |
| |
| |
| |
| |
| |
| |
| {'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, |
| '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, |
| '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, |
| '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')) |
|
|