| """harness/tables.py β a topic-backed TABLE for the grid (CG-2). |
| |
| The grid's first table (customers) is built from `modules.customer_data.pool()`: one row per |
| customer, the whole book shipped as component args, filtered client-side. That works at 1,550 |
| rows and does not work at line grain β `sales_lines` is 201,558 rows / ~89 MB. |
| |
| This module builds the SECOND table: rows come from `semantic.store_rows`, so the fetch is |
| WINDOWED and the filtering is SQL. Its whole job is to hand the grid a payload in the same |
| host-neutral shape the React component already speaks, plus the one thing a windowed table must |
| carry that a whole-book table never needed: |
| |
| counts = {matched, total, shown, windowed} |
| |
| `matched` and `total` come from their own queries over the FULL scope; `shown` is the size of |
| this window. The component renders "showing SHOWN of MATCHED (filtered from TOTAL)" from those |
| numbers and NEVER from `rows.length` β which is the difference between an honest window and a |
| silent `[:N]` wearing a total ([[no-unverifiable-aggregates]]). |
| |
| The field CONTRACT is derived from the topic, not hand-written: labels and types come from the |
| model's dims and metrics, so a new metric cannot arrive as an untyped, unlabelled column and the |
| grid can never disagree with the semantic layer about what a column means. |
| """ |
| from harness import semantic as _sem |
|
|
| |
| |
| TOPIC_TABLES = { |
| "sales_lines": { |
| "label": "Sales lines", |
| "storage_key": "sales_lines_table_workspace", |
| |
| "default_visible": ["date", "order_partner", "product", "category", "state", "agent", |
| "team", "revenue", "units", "margin"], |
| |
| "default_sorts": [{"colId": "date", "dir": "desc"}], |
| }, |
| } |
|
|
| |
| |
| |
| PAGE_ROWS = 200 |
|
|
|
|
| def _labels(topic): |
| """colId -> human label, taken from the MODEL (dim labels, metric labels).""" |
| t = _sem.topics()[topic] |
| out = {} |
| for key, d in ((t.get("store") or {}).get("dims") or {}).items(): |
| out[key] = d.get("label") or key.replace("_", " ").title() |
| out[f"{key}_id"] = f"{out[key]} id" |
| out["date"] = "Date" |
| for k, m in _sem.metrics().items(): |
| if m.get("topic") == topic: |
| out[k] = m.get("label") or k |
| return out |
|
|
|
|
| def table_fields(topic): |
| """The grid field contract for a topic, derived from the semantic model. |
| |
| Every column is `source='odoo'` β read-only. A line is a fact from the ERP; unlike the |
| customer table there is no overlay stratum here (notes/tags hang off a CUSTOMER, not off an |
| individual order line), so the two-strata split simply does not arise. |
| """ |
| cfg = TOPIC_TABLES[topic] |
| cols = _sem.store_columns(topic, grain="row") |
| labels = _labels(topic) |
| order = [k for k in cfg["default_visible"] if k in cols] |
| order += [k for k in cols if k not in order] |
| return [{ |
| "key": k, |
| "label": labels.get(k, k.replace("_", " ").title()), |
| "type": cols[k]["type"], |
| "source": "odoo", |
| "default": k in cfg["default_visible"], |
| } for k in order] |
|
|
|
|
| def table_payload(topic, config=None, offset=0, limit=None, team_id=None, |
| date_from=None, date_to=None): |
| """Fetch ONE window of a topic-backed table plus its scope-wide counts and totals. |
| |
| `config` is a grid ViewConfig (filters / filterConj / sorts / search) β the same shape the |
| customer table persists, so a saved view means the same thing on either table. |
| |
| Returns the component payload: {fields, rows, counts, aggregates, storageKey}. |
| """ |
| if topic not in TOPIC_TABLES: |
| raise _sem.ModelError(f"no topic-backed table registered for {topic!r}") |
| cfg = TOPIC_TABLES[topic] |
| config = config or {} |
| limit = PAGE_ROWS if limit is None else limit |
|
|
| got = _sem.store_rows( |
| topic, |
| filter_tree=config.get("filters") or [], |
| filter_conj=config.get("filterConj") or "and", |
| search=config.get("search") or None, |
| sorts=config.get("sorts") or cfg["default_sorts"], |
| member_ids=config.get("memberPids") or None, |
| limit=limit, offset=offset, |
| team_id=team_id, date_from=date_from, date_to=date_to, |
| ) |
|
|
| rows = [] |
| for i, r in enumerate(got["rows"]): |
| |
| |
| row = {k: v for k, v in r.items() if k != "_rid"} |
| row["pid"] = r["_rid"] |
| rows.append(row) |
|
|
| return { |
| "fields": table_fields(topic), |
| "rows": rows, |
| |
| |
| "counts": { |
| "shown": len(rows), |
| "matched": got["row_count"], |
| "total": got["total_count"], |
| "windowed": True, |
| }, |
| "aggregates": got["aggregates"], |
| "storageKey": cfg["storage_key"], |
| "label": cfg["label"], |
| "window": got["window"], |
| } |
|
|