Status: π‘ Live shell, integration-gated money features. The reporting core
and the cost-control panes (Reports, Cost Inbox, Cost Codes, Cost Catalog) are
fully wired and live against real data. Invoicing works end-to-end. But every
feature that moves money or leaves the building β online payment (Stripe),
e-signature (Documenso), AI bill capture (Gemini), and accounting sync
(QuickBooks Online) β is real code that stays dormant until a per-org
credential is set (all default empty). Xero is a scaffold only β no
implementation exists. So the hub is π’ as a workspace and π‘ as a
money-movement/accounting engine depending on how your org is configured.
Hub route: GET /portal/reports β renders reports_hub.html (the catalog)
inside reports_shell.html β app/routers/reports.py:60. ?classic=1 serves the
old flat catalog page (reports.py:67).
Shell: templates/reports_shell.html β persistent left rail + content region,
no iframe (reports_shell.html:8).
Rail source of truth: app/services/report_specs.rail_context()
(report_specs.py:1745), reused by the money/cost routers so the rail is identical
everywhere.
Purpose: One command center for analytics and back-office finance β the
native Reports module (which replaced Metabase, retired 2026-06-09) plus the
folded finance pages: Invoices, Purchase Orders, Cost Inbox, Lien Waivers, Cost
Codes, Cost Catalog, Signatures.
Unlike Team & HR (true inline Alpine panes), only the Reports experience is
native inside the shell (catalog β viewer β grid β drill, same rail). The seven
finance "panes" are standalone /portal/* pages reached from the shell's
Finance rail group, each marked β because it opens full-screen as its own
page (reports_shell.html:173-180). This matches the nav plan's rule that heavy
editors (Procurement CC, report viewer) open full-screen natively rather than as
inline panes (docs/nav_consolidation_plan.md:13-18). So "pane" below means "the
page linked from the Reports & Finance rail."
require_portal_role(pm, office)reports.py:29, catalog.py:22, cost.py:31, procurement.py:36. Thereport_specs.visible_specs returns [] for field β report_specs.py:1739)./portal/*; clients/vendors view, pay, and sign on tokenized public pagesreports.py:384).| Role | Access |
|---|---|
| owner | Everything, incl. report audit log + QBO connect/disconnect + payroll-to-QBO post. |
| pm / office | Reports, all finance panes, invoicing, cost inbox, catalog, waivers, signatures. |
| field | No back-office reports/finance. Field workers only feed the system (e.g. snap-a-receipt β Cost Inbox via /field/receipt, field.py:2413). |
| client / vendor | Public tokenized pages only: view+pay an invoice, sign a document/waiver. |
GET /portal/reports (reports.py:60); viewerGET /portal/reports/r/{id} (reports.py:90); interactive grid JSON.../data.json (reports.py:113); server drill .../drill (reports.py:149);.../insights (reports.py:164); native charts .../chartreports.py:186); KPI trends .../trends (reports.py:203); export.../export CSV/XLSX/PDF + custom (reports.py:219). Every report is aReportSpec (report_specs.py:48); the engine isapp/services/reporting.py (136 KB of real SQLAlchemy roll-ups)./portal/reports β browse the catalog (search +report_specs.py:37) β open a report β the viewer renders a sortable/filterablereports.py:253); schedule email bursts/portal/reports/subscriptions, reports.py:307); owners inspect the/portal/reports/audit, reports.py:379).report_specs.py:1491-1704. Country-specific specsreport_specs.py:1735).docs/* Metabase references are pre-retirement andreports.py:180 swallows failures).GET /portal/invoices (money.py:948); builderGET /portal/invoices/{id} (money.py:1027); create (manual/AI/frommoney.py:983 / 554 / 379 / 1292; sendPOST .../{id}/send (money.py:579); manual mark-paid .../mark-paidmoney.py:650). Public client view GET /invoice/{token}invoice_portal.py:34); pay GET /invoice/{token}/payinvoice_portal.py:107); Stripe webhook POST /stripe (webhooks.py:76)./portal/invoices β New (AI drafts title/cover/linesmoney.py:1006) β edit lines + theme βpaid (cardinvoice_portal.py:48-57). Progress billing:PaymentMilestone releases at 100% trigger β staff generate the draw invoicemoney.py:1292).stripe_client.create_checkout POSTs toapi.stripe.com/v1/checkout/sessions (stripe_client.py:35-69), hand-rolledstripe_client.py:88). Per-orgstripe_secret_key/stripe_webhook_secret default "" (config.py:192-193);invoice_portal.py:78,invoice_public.html:117).Invoice.proper_invoice_date is read for Ontariosent_at (models/money.py:190, read at reporting.py:1783). Invoice.currencyusd everywhere despite CAD framing. Two parallel create paths bothmoney.py:554 and :983)./portal/purchase-orders is an org-wide,procurement.py:117 βpurchase_orders.html. Actual PO management happens after click-through into thepurchase_orders.html:62 β /portal/jobs/{id}?view=command&folder=po).app/routers/procurement.py delegate toapp/services/purchase_orders.py. Gated pm/office + require_feature("procurement")procurement.py:36-37).POST /portal/jobs/{job_id}/po (procurement.py:419,party_kind) β add/replace lines while draftprocurement.py:490) β Release POST /portal/po/{id}/releaseprocurement.py:528): mints a public token, files the PO PDF into the jobGET /po/{token}, procurement.py:832)sub_portal.py:277) β Receive per-lineprocurement.py:587) β amend/close. E-signature = the canonicalprocurement.py:542)POStatus.billed is defined but never assigned anywherepurchase_orders.py:38). received/closed are set only viaprocurement.py:622); receiving quantities does notpurchase_orders.py:195-202) β so the enum's advertisedLIFECYCLE (purchase_orders.py:44) is partly aspirational at the tail. RetiredGET /portal/jobs/{id}/procurement is now a redirect shimprocurement.py:105).billed is dead.GET /portal/cost-inbox (cost.py:1100β¦ page atcost.py:115); capture POST /portal/bills/upload (cost.py:167); codePOST /portal/bills/{id} (cost.py:257); approve .../approvecost.py:363); mark-paid .../mark-paid (cost.py:380). Field on-rampGET/POST /field/receipt (field.py:2413). Gated pm/office; field excludedcost.py:31-34).Document(kind="bill") + a Bill(status=inbox) β AI extractionVendor), amount, currency, category, invoiceapproved, begins counting as actual cost) β optionally Mark paid (queues aapproved/paid bills roll into job costing (reporting.py:40).bill_capture.extract_billbill_capture.py:49) β integrations/ai/gemini.extract (real httpx POST togenerateContent, gemini.py:195). Config-gated: needs GEMINI_API_KEYconfig.py:113); without it, extraction returns {} and thebill_capture.py:58-62).bill_capture.py:1 docstring says "forwarded" invoices# --- Vendors --- section header with no routes (cost.py:497). The QBOcost.py:402).GET /portal/lien-waivers (lien_waivers.py:95); requested fromPOST /portal/bills/{id}/lien-waiver (lien_waivers.py:39) /.../invoices/{id}/lien-waiver (lien_waivers.py:65); refresh.../{wid}/sync (lien_waivers.py:114). Service services/lien_waivers.py.sync polls, downloads thedocumenso.py:33-141), not a stub, butDOCUMENSO_API_TOKEN is set β lw_svc.request returns Noneservices/lien_waivers.py:89); the panelien_waivers.html:10).LienWaiver rows, but the "Lien Waiver Tracking" reportBill.lien_waiver_statusreporting.py:2229, models/cost.py:345), fed by a separate no-loginlien.py:36-65). A Documenso-signed waiver doesGET /portal/cost-codes (cost.py:1100); add POST /cost-codescost.py:1065); seed defaults .../seed (cost.py:1088); clearcost.py:1113); reorder .../{id}/move (cost.py:1127); edit/deletecost.py:1156/1144). Engine services/costcodes.py.costcodes.py:146), then add/edit/reorder/suggest_code_for_item /guess_code (costcodes.py:246,305) power the catalog and cost inbox.current_portal_user.GET /portal/catalog (catalog.py:85); add/edit/delete itemcatalog.py:115/212/237); AI code .../suggest-code + .../auto-code-allcatalog.py:136/156); seed-from-history .../seed (catalog.py:179);catalog.py:192/203); save estimate line to catalogcatalog.py:265). Gated pm/office (catalog.py:22).services/catalog.py:21); AI auto-assigns cost codes per item or in bulk;_is_locked). The catalog drives line pricing elsewhere viaprice_index/resolve_lines (services/catalog.py:56).catalog.py:251) butmoney.py:497). Not broken, just asymmetric.GET /portal/signatures (app/portal/router.py:1329);.../{id} (router.py:1353); resend email.../{id}/resend (router.py:1384); SMS .../{id}/text (router.py:1414);.../{id}/download (router.py:1436); sync-allrouter.py:1459). Health diagnostics signature_health.assesssignature_health.py:84).SignatureRequest (filter by type/status) β proposals, invoices, contracts,models/signature.py:15). Staff send from each doc's own page (e.g. CO sendmoney.py:810, doc sign documents.py:1304, PO sign procurement.py:646);esign_service.request_* β documenso.create_and_send. RecipientssendEmail=False so it doesn't send its ownesign_service.py:51). POs/COs support dual counter-signing. Onesign_service.sync files the certified PDF and drives downstreamwebhooks.py:124, gated on DOCUMENSO_WEBHOOK_SECRET) and a scheduler pollscheduler.py:341).sign.gobuild.ca, config.py:242; internal API over documenso:3000). It is aintegrations/esign/documenso.py, 140 lines), not anrequest_* no-ops (return None) whendocumenso.is_configured() is false β documenso_api_token defaults ""config.py:241); the webhook 404s without DOCUMENSO_WEBHOOK_SECRET.signature_health.py is explicitly "Phase 0 read-onlysignature_health.py:1). request_invoice_signature is fully implemented butesign_service.py), worth confirming.httpx β token exchange tooauth.platform.intuit.com (integrations/quickbooks/client.py:72), API calls{qbo_api_base}/v3/company/{realm} (client.py:131-159), webhook HMACclient.py:162). Not a mock. Dormant only becauseqbo_client_id/qbo_client_secret are empty here (config.py:199-200,qbo_configured at config.py:205).quickbooks_sync.py:1):
quickbooks_sync.py:102-129, 159-233, 355-517, 537-718. Idempotent viaQuickBooksMap + SyncToken.Balance <= 0 (quickbooks_sync.py:732-785). Other entity changes arerouters/quickbooks.py:61-150) with an accounts-mapping form (cost category βQuickBooksConnection, UniqueConstraint(organization_id)). β οΈ Tokens aremodels/integration.py:8-9).money.py:665); bill push is a background task on bill-paidcost.py:402); payroll journal is an owner-only button in Team & HRrouter.py:3489); paid-state pull is webhook-driven (webhooks.py:56).config.py:215-224, "parallel to QBO"), butapp/integrations/xero/ contains only an empty __init__.py β no client, noxeroentitlements.py:28). xero_configuredconfig.py:222) has zero readers; the promised/portal/integrations/xero/callback route does not exist. For the wiki:cra_t5018.build_return_xml produces a real T619+T5018 information-return XMLcra_t5018.py:102) and slip PDFs (cra_t5018.py:168), wired as the t5018report_specs.py:661-668,1584). The1099-NEC twin exists too (report_specs.py:1665). It does not.xsd before filing (cra_t5018.py:1-15).| Item | Evidence | Note |
|---|---|---|
| Xero integration | app/integrations/xero/ empty; config.py:218-224; xero_configured unread |
Scaffold only β no code. |
| QBO plaintext tokens | models/integration.py:8-9 |
Acknowledged hardening TODO. |
| QBO webhook pull | quickbooks_sync.py:751-754 |
Only Invoices reconcile; other entities ignored. |
PO billed status |
purchase_orders.py:38 |
Enum value never assigned β unreachable. |
| PO receive/close | procurement.py:622, purchase_orders.py:195 |
Status advanced only via manual dropdown. |
| Cost Inbox "email-in" | bill_capture.py:1 docstring |
Wording only β no inbound path; upload/photo only. |
Cost Inbox # Vendors |
cost.py:497 |
Empty section header, no routes. |
Invoice proper_invoice_date |
models/money.py:190, read reporting.py:1783 |
Read for prompt-payment clock, never written. |
Invoice currency |
hardcoded usd in all create paths |
CAD framing in comments only. |
| Lien waiver two systems | pane LienWaiver vs report Bill.lien_waiver_status (reporting.py:2229) |
Disconnected data sources. |
request_invoice_signature |
esign_service.py (no router caller found) |
Possibly-unwired sender. |
signature_health |
signature_health.py:1 |
Phase-0, only classifies PO signatures. |
| Metabase docs | docs/metabase-*.md, docs/job-costing.md:52 |
Stale β native Reports retired Metabase 2026-06-09. |
| Feature | Setting (default) | Behaviour when unset |
|---|---|---|
| Online invoice payment | stripe_secret_key / stripe_webhook_secret = "" (config.py:192) |
Pay button hidden; manual check/wire instructions. |
| E-signature (all docs + waivers) | documenso_api_token = "" (config.py:241) |
request_* no-ops; "not configured" banners. |
| AI bill capture | GEMINI_API_KEY = "" (config.py:113) |
Extraction returns {}; manual bill entry. |
| QuickBooks sync | qbo_client_id / qbo_client_secret = "" (config.py:199) |
Connect shows "unconfigured"; pushes no-op. |
| Xero sync | xero_client_id / xero_client_secret = "" (config.py:218) |
No effect β feature does not exist. |