| """odoo_relational.py β Odoo entities as LOCKED relational databases. |
| |
| Owner ruling R1 / contract C8: spawn preset Odoo databases for Royal Imports, give them preset |
| Link + Rollup fields, and prove each rollup against the `measure_` column it will eventually |
| replace. |
| |
| ββ 2026-08-09 β THE POPULATIONS WIDENED FROM "OPEN AR" TO **EVERY ODOO ID**, which is the |
| owner's item: *"make sure we have all the Unique ID in Odoo in Database for the Royal Imports |
| tenant."* Before this change there were two tables holding 438 partners and 1,228 invoices β the |
| partners who owed money β so most Odoo ids were simply absent, and the missing rows were the |
| reason a Rollup could not answer a sales question. Four tables now, keyed on the Odoo id itself: |
| |
| ut_odoo_customers 2,465 rows 0.43 MB every partner with a confirmed order or a |
| posted customer document |
| ut_odoo_products 5,829 rows 1.20 MB every active product carrying a SKU code |
| ut_odoo_invoices 31,418 rows 8.68 MB EVERY posted customer invoice + refund |
| ut_odoo_orders 32,700 rows 7.50 MB every confirmed sale order |
| |
| β WHAT MADE THAT LEGAL, AND IT WAS NOT A BIGGER NUMBER. `MAX_ROWS` was 5,000 and this module's |
| own `plan()` refused above it β but the cap was never a property of the store (see the measured |
| banner on `core.user_tables.MAX_ROWS`; `ig_master` has run a 500,000-row bucket the whole time). |
| The cap is now 60,000, DERIVED from what a row actually weighs (β this line said 100,000 until |
| wave 28 β that was the FIRST candidate and its own derivation REJECTED it for clearing the memory |
| budget by 0.6%; the prose was written before the number lost, and two sibling files said it too). |
| The four tables together are |
| 17.81 MB in one `user_tables` document β real, bounded, and booked: the per-table row-key split |
| is D-87's next increment. β ORDER LINES REMAIN OUT (256,810 rows / 63.9 MB / 2.57 s per copy); |
| they are answered by the read-through rollup, which never copies a row. |
| |
| β THE EXCLUDED CHANNEL IS NOW A COLUMN, NOT A DELETION. `core.odoo.EXCLUDE_PARTNER_NAMES` puts |
| GIFTWARE DEALS (partner 6369 β the Amazon channel) outside WHOLESALE scope, and the old tables |
| dropped its rows entirely. Dropping them contradicts "every Odoo id", so the rows are kept and |
| carry **`wholesale_scope`** instead. β β READ THIS BEFORE COMPARING ANY TOTAL: that one partner |
| holds **$1,755,779.95 of the $2,347,608.49** raw open balance β 75% of it β across 25 invoices. |
| Wholesale open AR is $591,828.54. So a column total here will not equal the AR page unless you |
| filter `wholesale_scope`, and that is the scope difference, not a defect. `read_open_ar` keeps |
| excluding, because `modules/ar` is its oracle and an oracle answers ONE question. |
| |
| β AR SURVIVED THE WIDENING UNCHANGED, AND THAT IS MEASURED, NOT ASSUMED. `sum(residual)` over |
| ALL posted customer documents equals `sum(residual)` over `modules/ar._open_docs`' own predicate |
| **to the cent** ($2,347,608.49): zero posted rows carry a non-zero residual outside |
| `payment_state IN ('not_paid','partial')`, and zero rows inside it carry a residual of 0. So the |
| `ar_outstanding` rollup needs no condition. β THE COUNT AND THE DATE DO β `countall` over the |
| wider link would count 31,418 documents and call them open invoices, so those two rollups carry |
| the oracle's predicate as an explicit `payment_state` condition pair. |
| """ |
| import datetime as _dt |
|
|
| |
| |
| RI_SLUGS = ("", "royal-imports") |
|
|
| INVOICES_KEY = "ut_odoo_invoices" |
| CUSTOMERS_KEY = "ut_odoo_customers" |
| ORDERS_KEY = "ut_odoo_orders" |
| PRODUCTS_KEY = "ut_odoo_products" |
| |
| |
| |
| AGENTS_KEY = "ut_odoo_agents" |
| ACCOUNTS_KEY = "ut_odoo_accounts" |
| BILLS_KEY = "ut_odoo_bills" |
| VENDORS_KEY = "ut_odoo_vendors" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| ORDER_LINES_KEY = "ut_odoo_order_lines" |
| GL_LINES_KEY = "ut_odoo_gl_lines" |
|
|
| |
| JOIN_KEY = "partner_id" |
| |
| PRODUCT_JOIN_KEY = "product_id" |
| |
| |
| |
| AGENT_JOIN_KEY = "agent_id" |
| |
| VENDOR_JOIN_KEY = "vendor_id" |
| ACCOUNT_JOIN_KEY = "account_code" |
|
|
| |
| |
| _AR_OPEN = "payment_state IN ('not_paid','partial')" |
| _POSTED_DOCS = "state = 'posted' AND move_type IN ('out_invoice','out_refund')" |
| _AR_WHERE = f"{_POSTED_DOCS} AND {_AR_OPEN}" |
| _CONFIRMED = "state IN ('sale','done')" |
|
|
| |
| |
| |
| |
| |
| |
| MAX_SHRINK = 0.5 |
|
|
|
|
| def _ut(): |
| import core.user_tables as user_tables |
| return user_tables |
|
|
|
|
| def _iso_today(): |
| return _dt.date.today().strftime("%Y-%m-%d") |
|
|
|
|
| def _preset(field, flow="odoo_relational"): |
| """Stamp a field as machine-owned + preset β the `ut_ensure` lock_fields convention, so the |
| grid renders it grey and the preset walls refuse a rename or a delete.""" |
| field = dict(field) |
| field["automation"] = {"flowId": flow, "preset": True} |
| return field |
|
|
|
|
| |
| |
| |
| _OPEN_ONLY = {"conditions": [{"field": "payment_state", "op": "eq", "value": "not_paid"}, |
| {"field": "payment_state", "op": "eq", "value": "partial"}], |
| "conditionConj": "or"} |
|
|
|
|
| |
| |
| |
| |
| |
| def _scope_field(): |
| return {"key": "wholesale_scope", "label": "In wholesale scope", "type": "checkbox", |
| "source": "overlay", "default": False, |
| "description": "Unticked = the GIFTWARE DEALS / Amazon channel, which every wholesale " |
| "metric in this product excludes. The row is kept so no Odoo id is " |
| "missing; filter on this column to reconcile against the AR page."} |
|
|
|
|
| def _refreshed_field(): |
| return {"key": "refreshed", "label": "Refreshed", "type": "date", "source": "overlay", |
| "default": False, "description": "When this row was last reconciled against Odoo."} |
|
|
|
|
| def agent_fields(): |
| """One row per SALES AGENT, keyed on the `res.partner` id. |
| |
| β THE POPULATION IS A UNION OF TWO DISAGREEING SOURCES, and the disagreement is the reason it |
| is a union rather than a pick. MEASURED 2026-08-09: 16 partners carry commission lines, 17 |
| carry `res_partner.agent = TRUE`, and the union is 19 β so **2 agents earn commission without |
| the flag and 3 are flagged with no commission yet**. Either source alone silently drops real |
| agents. Same shape as `read_customers`' two document universes, for the same reason. |
| """ |
| return [_preset(f) for f in ( |
| {"key": "agent", "label": "Agent", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `res.partner` id. Also this row's id."}, |
| {"key": AGENT_JOIN_KEY, "label": "Odoo agent id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "flagged", "label": "Flagged in Odoo", "type": "checkbox", "source": "overlay", |
| "default": True, |
| "description": "Ticked = `res.partner.agent` is set. Unticked agents were found by " |
| "their commission lines instead - both are real, which is why this " |
| "table is the union of the two."}, |
| {"key": "commissioned", "label": "Has commission lines", "type": "checkbox", |
| "source": "overlay", "default": True}, |
| |
| |
| {"key": "customers", "label": "Customers", "type": "link", "source": "overlay", |
| "default": True, |
| "link": {"table": CUSTOMERS_KEY, "on": AGENT_JOIN_KEY, "from": AGENT_JOIN_KEY}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| def account_fields(): |
| """One row per `account.account` β the GL chart, the owner's "GL codes". |
| |
| β NO LINK COLUMN, and that is a finding rather than an omission. A GL account meets the rest |
| of this schema only at LINE grain (963,783 `account_move_line` rows, 154,917 of them on |
| expense-type accounts), and a `ut_*` link folds rows that live in the store. The honest |
| binding is a read-through rollup naming a governed topic, or the mirror grid (R2) β never a |
| link into a table that does not exist. Declaring one here would render a permanently blank |
| column, which is the exact trap D-87 warns about from the value site. |
| """ |
| return [_preset(f) for f in ( |
| {"key": "account_code", "label": "Code", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": "account_name", "label": "Account", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `account.account` id. Also this row's id."}, |
| |
| |
| |
| {"key": "account_type", "label": "Type", "type": "select", "source": "overlay", |
| "default": True, |
| "options": ["expense", "expense_direct_cost", "expense_depreciation", "income", |
| "income_other", "asset_cash", "asset_current", "asset_receivable", |
| "asset_fixed", "asset_non_current", "asset_prepayments", |
| "liability_current", "liability_payable", "liability_credit_card", |
| "liability_non_current", "equity", "equity_unaffected", "off_balance"]}, |
| {"key": "is_expense", "label": "Expense account", "type": "checkbox", "source": "overlay", |
| "default": True, |
| "description": "Ticked for the expense family - the same predicate the semantic layer's " |
| "gl_lines topic uses, so this column and that topic cannot disagree."}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| def vendor_fields(): |
| """One row per partner we have POSTED a vendor bill to, keyed on the `res.partner` id. |
| |
| β A VENDOR IS NOT A CUSTOMER TABLE ROW, even though both are `res.partner`. MEASURED: 393 |
| vendors, of which only 9 also appear in the customer population. Pointing bills at |
| `ut_odoo_customers` would have dangled 384 of 393 links β the failure would have been a mostly |
| empty column, not an error. |
| """ |
| return [_preset(f) for f in ( |
| {"key": "vendor", "label": "Vendor", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `res.partner` id. Also this row's id."}, |
| {"key": VENDOR_JOIN_KEY, "label": "Odoo vendor id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "country", "label": "Country", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": "bills", "label": "Bills", "type": "link", "source": "overlay", "default": True, |
| "link": {"table": BILLS_KEY, "on": VENDOR_JOIN_KEY, "from": VENDOR_JOIN_KEY}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| def bill_fields(): |
| """One row per POSTED vendor bill or refund β the owner's "expenses", at DOCUMENT grain. |
| |
| β DOCUMENT GRAIN IS A CHOICE AND IT IS THE ONLY ONE THAT FITS: 6,538 bills against 154,917 |
| expense GL lines. What a person calls "expenses" is both, and they are different tables - the |
| bill is what you pay, the line is what it was coded to. This is the payable; the line ledger |
| is the read-through mirror grid (R2). |
| """ |
| return [_preset(f) for f in ( |
| {"key": "bill_no", "label": "Bill", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `account.move` id. Also this row's id."}, |
| {"key": "vendor", "label": "Vendor", "type": "text", "source": "overlay", "default": True}, |
| {"key": VENDOR_JOIN_KEY, "label": "Odoo vendor id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "invoice_date", "label": "Bill date", "type": "date", "source": "overlay", |
| "default": True}, |
| {"key": "due_date", "label": "Due date", "type": "date", "source": "overlay", |
| "default": True}, |
| |
| |
| {"key": "amount_untaxed", "label": "Billed $", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| {"key": "residual", "label": "Outstanding $", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| {"key": "payment_state", "label": "Payment state", "type": "select", "source": "overlay", |
| "default": True, |
| "options": ["not_paid", "partial", "in_payment", "paid", "reversed"]}, |
| {"key": "move_type", "label": "Document", "type": "select", "source": "overlay", |
| "default": False, "options": ["in_invoice", "in_refund"]}, |
| {"key": "vendor_link", "label": "Vendor record", "type": "link", "source": "overlay", |
| "default": False, |
| "link": {"table": VENDORS_KEY, "on": VENDOR_JOIN_KEY, "from": VENDOR_JOIN_KEY}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| def invoice_fields(): |
| """One row per POSTED customer invoice or refund β the full history, not just what is open.""" |
| return [_preset(f) for f in ( |
| {"key": "invoice_no", "label": "Invoice", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `account.move` id. Also this row's id."}, |
| {"key": "customer", "label": "Customer", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": JOIN_KEY, "label": "Odoo partner id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "invoice_date", "label": "Invoice date", "type": "date", "source": "overlay", |
| "default": True}, |
| {"key": "due_date", "label": "Due date", "type": "date", "source": "overlay", |
| "default": True}, |
| {"key": "residual", "label": "Outstanding $", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum", |
| "description": "Odoo's signed residual. Exactly 0 on every settled document, which is " |
| "why AR rollups need no filter."}, |
| {"key": "amount_untaxed", "label": "Invoiced $", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| |
| |
| |
| |
| {"key": "payment_state", "label": "Payment state", "type": "select", "source": "overlay", |
| "default": True, |
| "options": ["not_paid", "partial", "in_payment", "paid", "reversed"]}, |
| {"key": "move_type", "label": "Document", "type": "select", "source": "overlay", |
| "default": False, "options": ["out_invoice", "out_refund"]}, |
| _scope_field(), |
| |
| |
| {"key": "customer_link", "label": "Customer record", "type": "link", "source": "overlay", |
| "default": False, |
| "link": {"table": CUSTOMERS_KEY, "on": JOIN_KEY, "from": JOIN_KEY}}, |
| |
| |
| |
| |
| |
| |
| |
| {"key": "origin_order", "label": "Source order", "type": "text", "source": "overlay", |
| "default": False, |
| "description": "Odoo's `invoice_origin` - usually the order name, sometimes blank."}, |
| {"key": "order_link", "label": "Order record", "type": "link", "source": "overlay", |
| "default": False, |
| "link": {"table": ORDERS_KEY, "on": "order_no", "from": "origin_order"}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| def order_fields(): |
| """One row per CONFIRMED sale order β `state in (sale, done)`, the fixed wholesale scope.""" |
| return [_preset(f) for f in ( |
| {"key": "order_no", "label": "Order", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `sale.order` id. Also this row's id."}, |
| {"key": "customer", "label": "Customer", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": JOIN_KEY, "label": "Odoo partner id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "order_date", "label": "Order date", "type": "date", "source": "overlay", |
| "default": True}, |
| {"key": "amount_untaxed", "label": "Order $", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| {"key": "team", "label": "Business unit", "type": "select", "source": "overlay", |
| "default": True, "options": ["Fisch", "Royal", "Sales", "Giftware Deals"]}, |
| {"key": "state", "label": "State", "type": "select", "source": "overlay", |
| "default": False, "options": ["sale", "done"]}, |
| {"key": "invoice_status", "label": "Invoice status", "type": "select", "source": "overlay", |
| "default": True, "options": ["invoiced", "to invoice", "upselling", "no"]}, |
| _scope_field(), |
| {"key": "customer_link", "label": "Customer record", "type": "link", "source": "overlay", |
| "default": False, |
| "link": {"table": CUSTOMERS_KEY, "on": JOIN_KEY, "from": JOIN_KEY}}, |
| |
| |
| {"key": "invoices", "label": "Invoices", "type": "link", "source": "overlay", |
| "default": True, |
| "link": {"table": INVOICES_KEY, "on": "origin_order", "from": "order_no"}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| def order_line_fields(): |
| """One row per `sale.order.line` on a CONFIRMED order β the same `state in (sale, done)` |
| scope every wholesale metric in this product uses. |
| |
| MEASURED on the mirror 2026-08-12: **254,189 lines in scope** of 256,810 (the 2,621 excluded |
| sit on draft/sent/cancelled orders). Every line in scope is on a `sale` order β zero `done` β |
| but `done` stays in the option list because it is in the SCOPE, and a filter offering only |
| what happens to be stored today goes stale the first time an order is marked done. |
| |
| β THE SCOPE, THE ORDER DATE AND THE ORDER NAME ALL LIVE ACROSS A JOIN. `sale_order_line` |
| carries no `state` at all (12 columns, measured), so the binding is a join to `sale_order` β |
| which is also what makes `order_no` a readable primary cell instead of a line id. |
| |
| β `qty` IS `int`, NOT `currency`, AND THAT IS A MEASUREMENT. 3,888 of 256,810 lines carry a |
| FRACTIONAL quantity (0.2, 0.4, 0.5, 1.66 β¦) and the minimum is -1.0, so the question "does the |
| type truncate?" had to be answered rather than assumed: it does not. `int` and `currency` both |
| render through the client's `numberText`, which rounds nothing without a `format.decimals` |
| bag β the only difference is the `$` a `currency` column prepends. A quantity is not money, so |
| it takes the type that does not paint one. |
| """ |
| return [_preset(f) for f in ( |
| {"key": "order_no", "label": "Order", "type": "text", "source": "overlay", |
| "default": True, "pinned": True, |
| "description": "The sale order this line belongs to. Zero orders have a blank name, " |
| "which is why it is the primary cell rather than the line id."}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `sale.order.line` id. Also this row's id."}, |
| {"key": "order_id", "label": "Odoo order id", "type": "int", "source": "overlay", |
| "default": False, |
| "description": "The `sale.order` id β the key `ut_odoo_orders` is keyed on."}, |
| {"key": "customer", "label": "Customer", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": JOIN_KEY, "label": "Odoo partner id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "product", "label": "Product", "type": "text", "source": "overlay", |
| "default": True, |
| "description": "Blank on the 101 section and note lines, which carry no product."}, |
| {"key": PRODUCT_JOIN_KEY, "label": "Odoo product id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "qty", "label": "Qty", "type": "int", "source": "overlay", "default": True, |
| "agg": "sum", |
| "description": "Ordered quantity. 3,888 lines carry a fraction and some are negative " |
| "(returns), so nothing here is rounded."}, |
| {"key": "price_subtotal", "label": "Line $", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| {"key": "margin", "label": "Margin $", "type": "currency", "source": "overlay", |
| "default": False, "agg": "sum", |
| "description": "Odoo's own line margin. Populated on every line."}, |
| {"key": "purchase_price", "label": "Unit cost", "type": "currency", "source": "overlay", |
| "default": False, |
| "description": "The cost Odoo priced this line's margin against, per unit."}, |
| {"key": "order_date", "label": "Order date", "type": "date", "source": "overlay", |
| "default": True}, |
| {"key": "state", "label": "State", "type": "select", "source": "overlay", |
| "default": False, "options": ["sale", "done"]}, |
| _scope_field(), |
| )] |
|
|
|
|
| def gl_line_fields(): |
| """One row per `account.move.line` β the general ledger, and the owner's "expenses" at the |
| grain a person can actually browse. |
| |
| MEASURED 2026-08-12: **963,783 lines**, of which 944,846 posted, 18,885 cancelled and 52 |
| draft. β UNSCOPED ON PURPOSE β a general ledger whose draft and cancelled entries are |
| invisible is a ledger that cannot be reconciled, so `parent_state` rides as a COLUMN and the |
| reader chooses. That is the same decision the binding states from the SQL side. |
| |
| β TWO COLUMNS ARE LEGITIMATELY BLANK ON REAL ROWS, named here so neither reads as a defect: |
| **61,911 lines carry no partner** (journal entries that are not about a customer), and |
| **1,727 carry no account** at all, which is also why `account_code` β the key |
| `ut_odoo_accounts` is keyed on β is blank on exactly those 1,727 and the join that supplies |
| it is a LEFT one. |
| |
| β `move_type` IS `text`, NOT `select`, and it is the `product_fields.category` argument: |
| five values exist today (`out_invoice` 515,634 Β· `entry` 414,992 Β· `in_invoice` 18,901 Β· |
| `out_refund` 14,061 Β· `in_refund` 195) and Odoo's enum is longer than what we happen to hold. |
| A select whose options go stale answers a filter with a list that cannot match a stored value |
| (wave-26 item 24). `line_type` and `parent_state` ARE selects because their option lists were |
| measured COMPLETE against the whole table. |
| """ |
| return [_preset(f) for f in ( |
| {"key": "entry", "label": "Entry", "type": "text", "source": "overlay", |
| "default": True, "pinned": True, |
| "description": "The journal entry this line belongs to. Never blank."}, |
| {"key": "odoo_id", "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `account.move.line` id. Also this row's id."}, |
| {"key": "move_id", "label": "Odoo entry id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "account", "label": "Account", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": ACCOUNT_JOIN_KEY, "label": "Account code", "type": "text", "source": "overlay", |
| "default": True, |
| "description": "The GL code, from the joined chart of accounts β the key " |
| "`ut_odoo_accounts` is keyed on. Blank on the 1,727 lines with no " |
| "account."}, |
| {"key": "customer", "label": "Partner", "type": "text", "source": "overlay", |
| "default": True, |
| "description": "Blank on the 61,911 lines that are not about a partner."}, |
| {"key": JOIN_KEY, "label": "Odoo partner id", "type": "int", "source": "overlay", |
| "default": False}, |
| {"key": "date", "label": "Date", "type": "date", "source": "overlay", "default": True}, |
| {"key": "debit", "label": "Debit", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| {"key": "credit", "label": "Credit", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum"}, |
| {"key": "balance", "label": "Balance", "type": "currency", "source": "overlay", |
| "default": True, "agg": "sum", |
| "description": "Debit minus credit, as Odoo stores it. Sums to zero over a whole entry."}, |
| {"key": "line_type", "label": "Line type", "type": "select", "source": "overlay", |
| "default": False, |
| "options": ["product", "cogs", "payment_term", "line_note", "line_section"]}, |
| {"key": "move_type", "label": "Document type", "type": "text", "source": "overlay", |
| "default": False}, |
| {"key": "parent_state", "label": "Entry state", "type": "select", "source": "overlay", |
| "default": True, "options": ["draft", "posted", "cancel"]}, |
| _scope_field(), |
| )] |
|
|
|
|
| def product_fields(): |
| """One row per `product.product`, keyed on its id β EVERY product, archived ones included. |
| |
| β THE ROW ID IS THE PRODUCT ID, NOT THE SKU CODE, and the difference is measurable: 12 codes |
| map to more than one product id (re-SKU / merge history). The code is what a human reads and |
| the id is what `sales_lines.product` groups by, so both are columns and only the id is the |
| identity. |
| |
| β THE PINNED COLUMN IS THE NAME, NOT THE SKU, and that is not a style choice. 62 products |
| carry no `default_code` at all (UBER CHARGE, Delivery Charges, PICK UP β¦) while ZERO carry a |
| blank name β measured. Pinning `code` would give those rows a blank primary cell, which is |
| exactly D-80: a first column nothing populates quietly becoming the row's identity |
| ([[fallback-that-became-the-rule]]). |
| """ |
| return [_preset(f) for f in ( |
| {"key": "product", "label": "Product", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": PRODUCT_JOIN_KEY, "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `product.product` id. Also this row's id, and what " |
| "every product rollup groups by."}, |
| {"key": "code", "label": "SKU", "type": "text", "source": "overlay", |
| "default": True, "description": "Odoo's `default_code`. Blank on the 62 charge/service " |
| "products that are not stocked SKUs."}, |
| {"key": "active", "label": "Active in Odoo", "type": "checkbox", "source": "overlay", |
| "default": True, |
| "description": "Unticked = archived. Archived products are kept because they still " |
| "carry sales history β two of them sold this year."}, |
| |
| |
| |
| {"key": "category", "label": "Category", "type": "text", "source": "overlay", |
| "default": True}, |
| {"key": "product_type", "label": "Type", "type": "select", "source": "overlay", |
| "default": False, "options": ["product", "consu", "service"]}, |
| {"key": "standard_price", "label": "Standard cost", "type": "currency", |
| "source": "overlay", "default": True}, |
| |
| |
| |
| {"key": "sales_ytd", "label": "Sales YTD", "type": "rollup", "source": "overlay", |
| "default": True, "agg": "sum", |
| "rollup": {"source": {"topic": "sales_lines", "measure": "revenue", |
| "groupBy": "product", "on": PRODUCT_JOIN_KEY, "window": "ytd"}}}, |
| {"key": "units_ytd", "label": "Units YTD", "type": "rollup", "source": "overlay", |
| "default": True, "agg": "sum", |
| "rollup": {"source": {"topic": "sales_lines", "measure": "units", |
| "groupBy": "product", "on": PRODUCT_JOIN_KEY, "window": "ytd"}}}, |
| {"key": "margin_ytd", "label": "Gross margin YTD $", "type": "rollup", "source": "overlay", |
| "default": True, "agg": "sum", |
| "rollup": {"source": {"topic": "sales_lines", "measure": "margin", |
| "groupBy": "product", "on": PRODUCT_JOIN_KEY, "window": "ytd"}}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| def customer_fields(): |
| """One row per partner Odoo has transacted with, keyed on the `res.partner` id. |
| |
| β Two DERIVED links (`on` declared), so the engine owns both cells and a human cannot edit a |
| relation Odoo already decided. The rollups come in two kinds on purpose: |
| * LINK rollups fold the rows in `ut_odoo_invoices` / `ut_odoo_orders` β they can answer |
| anything about a document the table holds, including a date rank; |
| * SOURCE rollups name a governed topic + metric and are answered by ONE grouped SQL query |
| over the whole mirror β they can answer a DATE-WINDOWED money question, which a link |
| rollup cannot, because a condition can only compare against a literal and a literal year |
| start is right until 1 January. |
| """ |
| return [_preset(f) for f in ( |
| {"key": "customer", "label": "Customer", "type": "text", "source": "overlay", |
| "default": True, "pinned": True}, |
| {"key": JOIN_KEY, "label": "Odoo ID", "type": "int", "source": "overlay", |
| "default": False, "description": "The `res.partner` id. Also this row's id."}, |
| {"key": "city", "label": "City", "type": "text", "source": "overlay", "default": True}, |
| {"key": "state", "label": "State", "type": "text", "source": "overlay", "default": True}, |
| {"key": "country", "label": "Country", "type": "text", "source": "overlay", |
| "default": False}, |
| {"key": "agent", "label": "Sales agent", "type": "text", "source": "overlay", |
| "default": True, |
| "description": "The customer's assigned agent (res.partner.agent_ids[0] β the " |
| "Customers-module convention)."}, |
| |
| |
| |
| |
| {"key": AGENT_JOIN_KEY, "label": "Odoo agent id", "type": "int", "source": "overlay", |
| "default": False}, |
| _scope_field(), |
|
|
| |
| {"key": "invoices", "label": "Invoices", "type": "link", "source": "overlay", |
| "default": True, |
| "link": {"table": INVOICES_KEY, "on": JOIN_KEY, "from": JOIN_KEY}}, |
| {"key": "orders", "label": "Orders", "type": "link", "source": "overlay", |
| "default": True, |
| "link": {"table": ORDERS_KEY, "on": JOIN_KEY, "from": JOIN_KEY}}, |
| |
| |
| {"key": "agent_link", "label": "Agent record", "type": "link", "source": "overlay", |
| "default": False, |
| "link": {"table": AGENTS_KEY, "on": AGENT_JOIN_KEY, "from": AGENT_JOIN_KEY}}, |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| {"key": "ar_outstanding", "label": "AR outstanding $ - all channels", "type": "rollup", |
| "source": "overlay", "default": True, "agg": "sum", |
| "description": "Open balance across every posted document, INCLUDING the Amazon " |
| "channel. Filter `In wholesale scope` to reconcile with the AR page.", |
| "rollup": {"link": "invoices", "field": "residual", "fn": "sum"}}, |
| {"key": "invoiced_all_time", "label": "Invoiced $ - all time", "type": "rollup", |
| "source": "overlay", "default": False, "agg": "sum", |
| "rollup": {"link": "invoices", "field": "amount_untaxed", "fn": "sum"}}, |
| |
| |
| |
| {"key": "open_invoices", "label": "Open invoices #", "type": "rollup", |
| "source": "overlay", "default": True, |
| "rollup": {"link": "invoices", "fn": "countall", **_OPEN_ONLY}}, |
| |
| |
| |
| {"key": "oldest_due", "label": "Oldest due date", "type": "rollup", "source": "overlay", |
| "default": True, |
| "rollup": {"link": "invoices", "field": "due_date", "fn": "latest", |
| "sortBy": "due_date", "sortDir": "asc", **_OPEN_ONLY}}, |
|
|
| |
| {"key": "order_count", "label": "Orders #", "type": "rollup", "source": "overlay", |
| "default": True, |
| "rollup": {"link": "orders", "fn": "countall"}}, |
| {"key": "last_order", "label": "Last order date", "type": "rollup", "source": "overlay", |
| "default": True, |
| "rollup": {"link": "orders", "field": "order_date", "fn": "latest", |
| "sortBy": "order_date", "sortDir": "desc"}}, |
|
|
| |
| |
| |
| |
| |
| {"key": "sales_ytd", "label": "Sales YTD - invoiced", "type": "rollup", |
| "source": "overlay", "default": True, "agg": "sum", |
| "rollup": {"source": {"topic": "sales_lines", "measure": "revenue_invoiced", |
| "groupBy": "order_partner", "on": JOIN_KEY, "window": "ytd"}}}, |
| {"key": "sales_ltm", "label": "Sales LTM", "type": "rollup", "source": "overlay", |
| "default": True, "agg": "sum", |
| "rollup": {"source": {"topic": "sales_lines", "measure": "revenue", |
| "groupBy": "order_partner", "on": JOIN_KEY, "window": "ltm"}}}, |
| {"key": "margin_ytd", "label": "Gross margin YTD $", "type": "rollup", "source": "overlay", |
| "default": False, "agg": "sum", |
| "rollup": {"source": {"topic": "sales_lines", "measure": "margin", |
| "groupBy": "order_partner", "on": JOIN_KEY, "window": "ytd"}}}, |
| {"key": "orders_ytd", "label": "Orders YTD #", "type": "rollup", "source": "overlay", |
| "default": False, |
| "rollup": {"source": {"topic": "sales_orders", "measure": "orders", |
| "groupBy": "partner", "on": JOIN_KEY, "window": "ytd"}}}, |
| _refreshed_field(), |
| )] |
|
|
|
|
| |
| |
| |
| def excluded_names(): |
| """The partner names out of wholesale scope, from the ONE place that defines them. |
| |
| Read through `core.odoo` rather than re-listed here: a second literal is a second scope, and |
| the day somebody adds a channel this module would keep answering the old question. |
| """ |
| try: |
| import core.odoo as odoo |
| names = getattr(odoo, "EXCLUDE_PARTNER_NAMES", None) or set() |
| return {str(n).strip().lower() for n in names if str(n).strip()} |
| except Exception: |
| return set() |
|
|
|
|
| def excluded_ids(cur, names=None): |
| """The out-of-scope partner IDS, resolved against the MIRROR. |
| |
| β IDS, NOT THE DENORMALISED NAME ON THE DOCUMENT, for two reasons that both bite. |
| `account_move.partner_name` is a copy taken when the document was written, and this module's |
| own `customers_from` says so out loud β *"a partner's name can differ across documents |
| (renames land on new invoices only)"*. So a rename would put some of one partner's documents |
| in scope and the rest out, silently, and the totals would stop reconciling with nothing to |
| point at. `modules/ar`, the oracle these numbers answer to, has always excluded by ID. |
| |
| β RESOLVED FROM THE MIRROR, NOT `core.odoo.excluded_partner_ids()`. That function issues a |
| LIVE `search_read`, so importing it here would make spawning four locked databases fail |
| whenever Odoo is unreachable β including on this developer machine, where the handshake dies |
| on an expired certificate ([[local-odoo-ssl-quirk]]). Same names, same answer, no network. |
| |
| β MATCHED CASE- AND WHITESPACE-INSENSITIVELY, and a NULL name simply does not match β which |
| is the correct direction. 44 transacting partners carry no name at all; treating an |
| unanswerable name as "excluded" would drop $7,734.83 of real open AR out of scope. |
| """ |
| names = names if names is not None else excluded_names() |
| if not names: |
| return set() |
| rows = cur.execute("SELECT id, name FROM res_partner WHERE name IS NOT NULL").fetchall() |
| return {int(pid) for pid, name in rows if str(name).strip().lower() in names} |
|
|
|
|
| def columns(cur, table): |
| """The column names a mirror table actually has, lowercased. `set()` if the table is absent. |
| |
| β WHY THIS EXISTS, AND IT COST A LIVE 500. `harness.datastore.ready()` gates on ENTITY |
| phases, and a Space hydrates its mirror from `store_seed/royal.duckdb` β a SNAPSHOT. Columns |
| added to `ENTITIES` after that snapshot was taken (`res_partner.agent_id`, |
| `account_move_line.product_id`, β¦) are backfilled by `sync_all()` under their OWN `_sync_state` |
| keys, which `ready()` does not read. So there is a real window, right after a boot, where the |
| store reports READY and a column this module names does not exist yet β and DuckDB answers a |
| missing identifier with a Binder error, which reached the operator as a bare `500`. |
| β The absent columns are all DISPLAY ones (an agent name, a category, a team). Refusing the |
| whole spawn over a cosmetic column would be worse than the gap it is reporting, so the readers |
| degrade the COLUMN to blank and still write every id. |
| """ |
| try: |
| rows = cur.execute(f"SELECT * FROM {table} LIMIT 0") |
| return {str(d[0]).lower() for d in rows.description} |
| except Exception: |
| return set() |
|
|
|
|
| def _col(have, name, default="NULL"): |
| """`name` when the mirror has it, else a literal that keeps the SELECT's arity intact.""" |
| return name if str(name).split(".")[-1].lower() in have else default |
|
|
|
|
| def _as_date(value): |
| """ISO date string, or ''. The grid renders `date` cells itself (W26: `Aug 5, 2026`), so the |
| STORED value stays ISO β a formatted string in the cell is a value the filters cannot sort.""" |
| if not value: |
| return "" |
| return str(value)[:10] |
|
|
|
|
| def _in_scope(pid, excluded): |
| """`'1'` | `''` β the `checkbox` cell convention (`aios_grid`: the overlay stores '1' or '').""" |
| return "" if int(pid) in excluded else "1" |
|
|
|
|
| def read_invoices(cur, excluded=None, open_only=False): |
| """[(row dict)] β posted customer invoices and refunds, keyed on the `account.move` id. |
| |
| ONE reader, two projections. `open_only` applies the AR oracle's predicate and drops the |
| out-of-scope channel, which is what `read_open_ar` wants; the default keeps every row and |
| TAGS the channel instead. Two queries would be two populations, and they drift the moment |
| either is edited. |
| |
| Takes a CURSOR so a gate can hand it a fixture connection; no global store binding here. |
| """ |
| excluded = excluded if excluded is not None else excluded_ids(cur) |
| where = _AR_WHERE if open_only else _POSTED_DOCS |
| |
| |
| |
| |
| |
| have = columns(cur, "account_move") |
| sql = ("SELECT id, name, partner_id, partner_name, invoice_date, invoice_date_due, " |
| " amount_untaxed_signed, amount_residual_signed, payment_state, move_type, " |
| f" {_col(have, 'invoice_origin', chr(39) + chr(39))} " |
| f"FROM account_move WHERE {where} AND partner_id IS NOT NULL") |
| out = [] |
| for r in cur.execute(sql).fetchall(): |
| (mid, name, pid, pname, inv_date, due, untaxed, residual, pay_state, mtype, origin) = r |
| scope = _in_scope(pid, excluded) |
| if open_only and not scope: |
| continue |
| out.append({ |
| "_id": str(mid), |
| "invoice_no": str(name or ""), |
| "odoo_id": int(mid), |
| "customer": str(pname or ""), |
| JOIN_KEY: int(pid), |
| "invoice_date": _as_date(inv_date), |
| "due_date": _as_date(due), |
| "residual": float(residual or 0.0), |
| "amount_untaxed": float(untaxed or 0.0), |
| "payment_state": str(pay_state or ""), |
| "move_type": str(mtype or ""), |
| "origin_order": str(origin or "").strip(), |
| "wholesale_scope": scope, |
| }) |
| return out |
|
|
|
|
| def read_open_ar(cur, excluded=None): |
| """The OPEN, wholesale-scoped subset β `modules/ar._open_docs`' own population. |
| |
| Kept as its own door because `modules/ar` is this module's oracle for the AR numbers, and an |
| oracle answers exactly one question. It is a projection of `read_invoices`, never a second |
| query. |
| """ |
| return read_invoices(cur, excluded=excluded, open_only=True) |
|
|
|
|
| def read_orders(cur, excluded=None): |
| """[(row dict)] β confirmed sale orders, keyed on the `sale.order` id.""" |
| excluded = excluded if excluded is not None else excluded_ids(cur) |
| have = columns(cur, "sale_order") |
| sql = (f"SELECT id, name, date_order, partner_id, partner_name, {_col(have, 'team_name')}, " |
| f" state, amount_untaxed, {_col(have, 'invoice_status')} " |
| f"FROM sale_order WHERE {_CONFIRMED} AND partner_id IS NOT NULL") |
| out = [] |
| for r in cur.execute(sql).fetchall(): |
| (oid, name, when, pid, pname, team, state, untaxed, inv_status) = r |
| out.append({ |
| "_id": str(oid), |
| "order_no": str(name or ""), |
| "odoo_id": int(oid), |
| "customer": str(pname or ""), |
| JOIN_KEY: int(pid), |
| "order_date": _as_date(when), |
| "amount_untaxed": float(untaxed or 0.0), |
| "team": str(team or ""), |
| "state": str(state or ""), |
| "invoice_status": str(inv_status or ""), |
| "wholesale_scope": _in_scope(pid, excluded), |
| }) |
| return out |
|
|
|
|
| def read_products(cur): |
| """[(row dict)] β EVERY `product.product`, keyed on its id. |
| |
| β NO `active` AND NO `default_code` FILTER, and both exclusions were measured before they |
| were dropped. Filtering to active-and-coded gave 5,829 of 5,948 rows and left FIVE products |
| that sold this very year with no row at all: two archived SKUs (`9SAT-FY`, `2GSTY`) and three |
| uncoded charge lines (`UBER CHARGE`, `[Delivery_009] Delivery Charges`, `PICK UP`). A product |
| grouped by `sales_lines.product` that has no parent row is a rollup value with nowhere to |
| land β silently. 119 extra rows is the whole cost of the claim being literally true. |
| """ |
| have = columns(cur, "product_product") |
| sql = (f"SELECT id, default_code, name, {_col(have, 'categ_name')}, type, " |
| f" {_col(have, 'standard_price', '0')}, {_col(have, 'active', 'TRUE')} " |
| "FROM product_product") |
| out = [] |
| for r in cur.execute(sql).fetchall(): |
| (prid, code, name, categ, ptype, cost, active) = r |
| out.append({ |
| "_id": str(prid), |
| "product": str(name or ""), |
| PRODUCT_JOIN_KEY: int(prid), |
| "code": str(code or ""), |
| "active": "1" if active else "", |
| "category": str(categ or ""), |
| "product_type": str(ptype or ""), |
| "standard_price": float(cost or 0.0), |
| }) |
| return out |
|
|
|
|
| def read_customers(cur, excluded=None): |
| """[(row dict)] β every CUSTOMER partner, keyed on the `res.partner` id. |
| |
| β THE POPULATION IS A UNION OF THREE LEGS, and every one of them is load-bearing. |
| |
| The two DOCUMENT legs are the original pair: Amazon books as direct invoices with no sale |
| order (the `odoo-api` gotcha), so a sale-order leg alone would silently drop a real customer. |
| |
| ββ THE THIRD IS `customer_rank > 0 AND active` (wave 29, item 22 / R12 via finding F2 β |
| the owner's *"never an arbitrary limit⦠applies to ALL connected database"*). The old |
| docstring said partners with no document are *"left out on purpose β a row that can never |
| appear in any topic has nothing to roll up"*; that reasoning is RETIRED. It is the same |
| join-drop class as the Product grid's 2,717, and it dropped **~1,149 real customer records** |
| (MEASURED 2026-08-11: `rank>0 active` = 3,617 against a document union of ~2,000). A customer |
| a salesperson has not sold to yet is exactly the row a prospecting view needs. |
| |
| β IT IS A UNION AND NOT A REPLACEMENT, AND THAT IS MEASURED, NOT TIDINESS. Swapping the |
| document legs for the rank leg would drop **16 partners that hold posted documents** (7 |
| archived, 9 active with rank <= 0), and `ut_odoo_invoices` / `ut_odoo_orders` rows carry |
| `partner_id` LINKS straight back here β so those links would dangle with nothing reporting |
| it. When other tables point AT a population, a widening must be a SUPERSET. |
| |
| β THE RANK LEG IS SKIPPED WHEN THE MIRROR HAS NO `customer_rank` COLUMN, which is the same |
| `columns()`/`_col` discipline every other optional column here uses β but note the difference |
| honestly: an absent `agent_id` blanks a CELL, while an absent `customer_rank` narrows the |
| POPULATION back to the document union. It degrades to today's behaviour rather than to an |
| empty or a wrong table, and `verify_odoo_relational` carries a check that goes RED while the |
| column is missing so the narrowing can never pass for done. |
| |
| β NOT FROM LIVE ODOO, THOUGH `customer_rank` IS TRIVIAL TO ASK IT. `excluded_ids` above |
| states the rule for this module and it applies with more force to a POPULATION than to a name |
| list: a live call makes the spawn fail whenever Odoo is unreachable, and a Space hydrates its |
| mirror from a SNAPSHOT at boot. The population would then be "whichever source answered this |
| time" β swinging ~45% against `MAX_SHRINK`'s 50% refusal, deleting and re-adding rows on the |
| weather. One source, always present at spawn time: the mirror. |
| |
| β NOT DERIVED FROM THE INVOICE ROWS. `customers_from` did that when the table WAS the open-AR |
| partners; sourcing a customer registry from its own receivables is what kept most Odoo ids |
| out of the store in the first place. |
| """ |
| excluded = excluded if excluded is not None else excluded_ids(cur) |
| have = columns(cur, "res_partner") |
| |
| |
| agent = ("ag.name" if "agent_id" in have else "NULL") |
| agent_id_col = ("p.agent_id" if "agent_id" in have else "NULL") |
| join = ("LEFT JOIN res_partner ag ON ag.id = p.agent_id " if "agent_id" in have else "") |
| |
| |
| |
| rank_leg = (" OR (p.customer_rank > 0 AND p.active) " |
| if {"customer_rank", "active"} <= have else "") |
| sql = (f"SELECT p.id, p.name, {_col(have, 'p.city')}, {_col(have, 'p.state_name')}, " |
| f" {_col(have, 'p.country_name')}, {agent}, {agent_id_col} " |
| "FROM res_partner p " |
| f"{join}" |
| "WHERE p.id IN (" |
| f" SELECT partner_id FROM sale_order WHERE {_CONFIRMED} AND partner_id IS NOT NULL " |
| " UNION " |
| " SELECT partner_id FROM account_move " |
| f" WHERE {_POSTED_DOCS} AND partner_id IS NOT NULL)" |
| f"{rank_leg}") |
| out = [] |
| for r in cur.execute(sql).fetchall(): |
| (pid, name, city, state, country, agent, agent_id) = r |
| out.append({ |
| "_id": str(pid), |
| "customer": str(name or ""), |
| JOIN_KEY: int(pid), |
| "city": str(city or ""), |
| "state": str(state or ""), |
| "country": str(country or ""), |
| "agent": str(agent or ""), |
| AGENT_JOIN_KEY: int(agent_id) if agent_id else "", |
| "wholesale_scope": _in_scope(pid, excluded), |
| }) |
| return out |
|
|
|
|
| def read_agents(cur): |
| """[(row dict)] β the UNION of both agent sources, keyed on the `res.partner` id. |
| |
| β `res_partner.agent` is a BOOLEAN and `datastore.BOOL_FIELDS` lists it for a measured reason: |
| Odoo returns False both for "empty" and for "boolean false", so a bool missing from that list |
| silently becomes NULL and every row would read "not an agent" indistinguishably from |
| "unknown". Read it as a truth value, never as a presence test. |
| """ |
| have = columns(cur, "res_partner") |
| if "id" not in have: |
| return [] |
| flagged = "p.agent" if "agent" in have else "FALSE" |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| has_comm = bool(columns(cur, "account_invoice_line_agent")) |
| commissioned = ("(p.id IN (SELECT agent_id FROM account_invoice_line_agent " |
| " WHERE agent_id IS NOT NULL))" if has_comm else "FALSE") |
| union_leg = (" SELECT agent_id FROM account_invoice_line_agent WHERE agent_id IS NOT NULL " |
| " UNION " if has_comm else "") |
| sql = (f"SELECT p.id, p.name, {flagged}, {commissioned} AS commissioned " |
| "FROM res_partner p WHERE p.id IN (" |
| f"{union_leg}SELECT id FROM res_partner WHERE {flagged})") |
| out = [] |
| for (aid, name, flag, comm) in cur.execute(sql).fetchall(): |
| out.append({ |
| "_id": str(aid), |
| "agent": str(name or ""), |
| "odoo_id": int(aid), |
| AGENT_JOIN_KEY: int(aid), |
| "flagged": "1" if flag else "", |
| "commissioned": "1" if comm else "", |
| }) |
| return out |
|
|
|
|
| def read_accounts(cur): |
| """[(row dict)] β the whole GL chart, keyed on the `account.account` id. |
| |
| β THE EXPENSE PREDICATE IS THE SEMANTIC LAYER'S, copied rather than invented: |
| `harness/semantic.py`'s `gl_lines` topic scopes expenses as |
| `account_type in ('expense','expense_depreciation')`. A second definition here is how a |
| column and a topic start disagreeing about the same word. |
| β `account.account` has NO `active` column in this Odoo version (a domain naming it 500s), so |
| there is nothing to filter and every account is a row. |
| """ |
| have = columns(cur, "account_account") |
| if not have: |
| return [] |
| sql = (f"SELECT id, {_col(have, 'code', chr(39) + chr(39))}, " |
| f" {_col(have, 'name', chr(39) + chr(39))}, " |
| f" {_col(have, 'account_type', chr(39) + chr(39))} FROM account_account") |
| out = [] |
| for (aid, code, name, atype) in cur.execute(sql).fetchall(): |
| t = str(atype or "") |
| out.append({ |
| "_id": str(aid), |
| ACCOUNT_JOIN_KEY: str(code or ""), |
| "account_name": str(name or ""), |
| "odoo_id": int(aid), |
| "account_type": t, |
| "is_expense": "1" if t in ("expense", "expense_depreciation") else "", |
| }) |
| return out |
|
|
|
|
| _VENDOR_DOCS = "state = 'posted' AND move_type IN ('in_invoice','in_refund')" |
|
|
|
|
| def read_bills(cur): |
| """[(row dict)] β posted vendor bills and refunds, keyed on the `account.move` id.""" |
| have = columns(cur, "account_move") |
| if not have: |
| return [] |
| sql = ("SELECT id, name, partner_id, partner_name, invoice_date, invoice_date_due, " |
| f" {_col(have, 'amount_untaxed_signed', '0')}, " |
| f" {_col(have, 'amount_residual_signed', '0')}, " |
| f" {_col(have, 'payment_state', chr(39) + chr(39))}, move_type " |
| f"FROM account_move WHERE {_VENDOR_DOCS} AND partner_id IS NOT NULL") |
| out = [] |
| for r in cur.execute(sql).fetchall(): |
| (mid, name, pid, pname, when, due, untaxed, residual, pay, mtype) = r |
| out.append({ |
| "_id": str(mid), |
| "bill_no": str(name or ""), |
| "odoo_id": int(mid), |
| "vendor": str(pname or ""), |
| VENDOR_JOIN_KEY: int(pid), |
| "invoice_date": _as_date(when), |
| "due_date": _as_date(due), |
| "amount_untaxed": float(untaxed or 0.0), |
| "residual": float(residual or 0.0), |
| "payment_state": str(pay or ""), |
| "move_type": str(mtype or ""), |
| }) |
| return out |
|
|
|
|
| def read_vendors(cur): |
| """[(row dict)] β every partner carrying a posted vendor bill, keyed on the `res.partner` id. |
| |
| β DERIVED FROM THE BILLS, unlike `read_customers` which is deliberately NOT derived from its |
| invoices. The asymmetry is intentional and the reason is what that function's own comment |
| says: a customer registry sourced from receivables is what kept most Odoo ids out of the store. |
| There is no second document universe for vendors β a partner with no bill has no payable |
| history to show β so the bill IS the population, and MEASURED it dangles nothing (0 bills |
| carry a null partner; all 393 vendors resolve in `res_partner`). |
| """ |
| have = columns(cur, "res_partner") |
| if not have or not columns(cur, "account_move"): |
| return [] |
| sql = (f"SELECT p.id, p.name, {_col(have, 'p.country_name', chr(39) + chr(39))} " |
| "FROM res_partner p WHERE p.id IN " |
| f" (SELECT partner_id FROM account_move WHERE {_VENDOR_DOCS} " |
| " AND partner_id IS NOT NULL)") |
| out = [] |
| for (pid, name, country) in cur.execute(sql).fetchall(): |
| out.append({ |
| "_id": str(pid), |
| "vendor": str(name or ""), |
| "odoo_id": int(pid), |
| VENDOR_JOIN_KEY: int(pid), |
| "country": str(country or ""), |
| }) |
| return out |
|
|
|
|
| def customers_from(invoice_rows): |
| """The partners carrying the given invoice rows β the pre-2026-08-09 population builder. |
| |
| β NO LONGER WHAT SPAWNS `ut_odoo_customers` (that is `read_customers`). Kept because it is a |
| pure function over rows and the gate uses it to prove the FOLD against a fixture without a |
| mirror; deleting it would cost a test its independence from the SQL. |
| """ |
| out = {} |
| for row in invoice_rows: |
| pid = row[JOIN_KEY] |
| entry = out.setdefault(str(pid), {"_id": str(pid), "customer": row["customer"], |
| JOIN_KEY: pid}) |
| |
| |
| if row["customer"]: |
| entry["customer"] = row["customer"] |
| return list(out.values()) |
|
|
|
|
| |
| |
| |
| class Refused(Exception): |
| """A refusal a caller should SHOW, not swallow. Every raise names what would otherwise have |
| been written wrong.""" |
|
|
|
|
| |
| |
| |
| TABLES = ( |
| ("customers", CUSTOMERS_KEY, "Odoo customers", customer_fields), |
| ("products", PRODUCTS_KEY, "Odoo products", product_fields), |
| ("invoices", INVOICES_KEY, "Odoo invoices", invoice_fields), |
| ("orders", ORDERS_KEY, "Odoo orders", order_fields), |
| |
| |
| |
| ("agents", AGENTS_KEY, "Odoo agents", agent_fields), |
| ("accounts", ACCOUNTS_KEY, "Odoo GL accounts", account_fields), |
| ("vendors", VENDORS_KEY, "Odoo vendors", vendor_fields), |
| ("bills", BILLS_KEY, "Odoo vendor bills", bill_fields), |
| |
| |
| |
| |
| |
| |
| ("order_lines", ORDER_LINES_KEY, "Odoo order lines", order_line_fields), |
| ("gl_lines", GL_LINES_KEY, "Odoo GL lines", gl_line_fields), |
| ) |
|
|
| |
| |
| |
| _READERS = { |
| "customers": lambda cur, excluded: read_customers(cur, excluded=excluded), |
| "products": lambda cur, excluded: read_products(cur), |
| "invoices": lambda cur, excluded: read_invoices(cur, excluded=excluded), |
| "orders": lambda cur, excluded: read_orders(cur, excluded=excluded), |
| "agents": lambda cur, excluded: read_agents(cur), |
| "accounts": lambda cur, excluded: read_accounts(cur), |
| "vendors": lambda cur, excluded: read_vendors(cur), |
| "bills": lambda cur, excluded: read_bills(cur), |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| READ_THROUGH_KEYS = frozenset(key for bucket, key, _l, _f in TABLES if bucket not in _READERS) |
|
|
|
|
| class _LentDoc: |
| """A store handle that serves the ONE `user_tables` document `plan()` has ALREADY read. |
| |
| ββ THIS IS NOT A MICRO-OPTIMISATION AND IT IS NOT OPTIONAL. `core.user_tables.row_limit` |
| resolves through `materialises` β `get` β `all_tables(st)`, and every one of those is a WHOLE |
| 20 MB document read, deep-copied under `Store._lock`. `plan()` asks the evaluator once per |
| table per loop, so passing the live handle would have added ~16 full document copies to a |
| function that already reads it exactly once β and with `st=None` (the gate's fixture posture, |
| and any dry run) those reads resolve to the MODULE-GLOBAL store, i.e. a Hugging Face dataset |
| fetch per table, on a path that has no business touching the network at all. |
| `materialises`' own docstring asks callers to lend the definition they are holding; `row_limit` |
| takes `st` rather than `defn`, so the lending happens one level up, here. |
| |
| β It answers ONLY the user-tables document and `None` for anything else, deliberately: a shim |
| that quietly proxied other keys would be a second store with a partial view, which is worse |
| than one that says what it knows. |
| """ |
|
|
| def __init__(self, doc, key): |
| self._doc, self._key = doc if isinstance(doc, dict) else {}, key |
|
|
| def get(self, name): |
| return self._doc if name == self._key else None |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| CONFIG_KEY = "odoo_connector_config" |
|
|
| |
| |
| |
| |
| |
| |
| SYNC_PRESETS = {"30m": 1800, "1h": 3600, "4h": 14400, "daily": 86400, "manual": None} |
| SYNC_FLOOR_SECONDS = 1800 |
| DEFAULT_SYNC = "30m" |
|
|
|
|
| def read_config(rt): |
| """This tenant's stored connector config, defaulted. Never raises β a store hiccup must not |
| make a connector look disconnected.""" |
| try: |
| cur = rt.get(CONFIG_KEY) if rt is not None else None |
| except Exception: |
| cur = None |
| cur = cur if isinstance(cur, dict) else {} |
| grids = cur.get("grids") if isinstance(cur.get("grids"), dict) else {} |
| every = cur.get("syncEvery") |
| return {"grids": {str(k): bool(v) for k, v in grids.items()}, |
| "syncEvery": every if every in SYNC_PRESETS else DEFAULT_SYNC, |
| "frozen": bool(cur.get("frozen")), |
| "frozenAt": str(cur.get("frozenAt") or "")} |
|
|
|
|
| def grid_choices(rt): |
| """`[{key, label, enabled}]` for every grid this connector can materialise β DERIVED from |
| `TABLES`, never a second hand-typed list (contract C2's parity leg asserts exactly that). |
| |
| β ABSENT MEANS ENABLED. A tenant that has never opened the panel has every grid, which is |
| what they have today; only an explicit untick turns one off. The alternative β an empty |
| config meaning "nothing enabled" β would silently unspawn ten live databases on deploy. |
| """ |
| chosen = read_config(rt)["grids"] |
| return [{"key": key, "label": label, "enabled": bool(chosen.get(key, True))} |
| for _bucket, key, label, _fields in TABLES] |
|
|
|
|
| def enabled_buckets(rt): |
| """The BUCKET names `plan()` speaks, for the grids this tenant has left ticked.""" |
| chosen = read_config(rt)["grids"] |
| return {bucket for bucket, key, _l, _f in TABLES if chosen.get(key, True)} |
|
|
|
|
| def sync_seconds(rt): |
| """How often this tenant's Odoo mirror should resync, or None for `manual` (R11). |
| |
| β THE FLOOR IS ENFORCED HERE AS WELL AS AT THE WRITE DOOR, deliberately. A stored value that |
| predates the preset list, or one written by any path that is not the route, must still not be |
| able to ask this loop for a 60-second cycle β a limit with only one enforcer is a limit that |
| holds until somebody finds the second way in [[limit-with-no-enforcer]]. |
| """ |
| secs = SYNC_PRESETS.get(read_config(rt)["syncEvery"], SYNC_PRESETS[DEFAULT_SYNC]) |
| if secs is None: |
| return None |
| return max(int(secs), SYNC_FLOOR_SECONDS) |
|
|
|
|
| def frozen(rt): |
| """Has this tenant DISCONNECTED Odoo (R10)? Frozen grids keep every row and every field and |
| stop being refreshed β distinct from PAUSED, which is temporary and keeps the credential.""" |
| return bool(read_config(rt)["frozen"]) |
|
|
|
|
| def plan(cur, rt=None): |
| """The rows that WOULD be written, plus the refusals that apply β no store WRITE at all. |
| |
| Separated from `apply_plan` so a route, a gate and a dry run all measure the same thing, and |
| so **every cap is checked before anything is committed**. |
| |
| Returns one key per bucket plus two that are not buckets: `problems` (refusals β a non-empty |
| list makes `apply_plan` raise before it writes anything) and, since W30-T35, **`limits`** β |
| `{table_key: limit_report}` for every table this plan did not fully materialise, which is R6's |
| second sentence carried as data rather than left for a reader to infer from an empty list. |
| |
| β `rt` IS WHAT MAKES THE TABLE-COUNT CHECK HONEST, and leaving it out was a real half-spawn |
| bug. This spawn writes FOUR tables in four updater passes; a tenant near `MAX_TABLES` would |
| create some and refuse the rest β leaving a locked invoices database with no rollup host, |
| while the route answered as though nothing had happened. A partial spawn is worse than a |
| refused one, so the count is checked against the tables that ALREADY exist, before the first |
| write. `rt` also carries the stored row counts the shrink guard compares against. |
| """ |
| ut = _ut() |
| |
| |
| |
| |
| |
| |
| |
| |
| ut.register_connected(*[key for _b, key, _l, _f in TABLES]) |
| excluded = excluded_ids(cur) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| existing = {} |
| if rt is not None: |
| try: |
| existing = dict(rt.get(ut.STORE_KEY) or {}) |
| except Exception: |
| existing = {} |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| lent = _LentDoc({**existing, |
| **{k: {**(existing.get(k) or {}), "readThrough": True} |
| for k in READ_THROUGH_KEYS}}, |
| ut.STORE_KEY) |
|
|
| built, limits = {}, {} |
| caps = {} |
| for bucket, key, _label, _fields in TABLES: |
| reader = _READERS.get(bucket) |
| caps[key] = cap = ut.row_limit(key, st=lent) |
| if reader is None or cap == 0: |
| built[bucket] = [] |
| report = ut.limit_report(key, st=lent) |
| if report: |
| limits[key] = report |
| continue |
| built[bucket] = reader(cur, excluded) |
| problems = [] |
|
|
| if rt is not None: |
| needed = [k for _b, k, _l, _f in TABLES if k not in existing] |
| if needed and len(existing) + len(needed) > ut.MAX_TABLES: |
| problems.append( |
| f"tenant holds {len(existing)} of MAX_TABLES={ut.MAX_TABLES} user tables and " |
| f"needs {len(needed)} more ({', '.join(needed)}); refusing rather than creating " |
| f"part of a linked set") |
|
|
| for bucket, key, _label, _fields in TABLES: |
| rows = built[bucket] |
| cap = caps[key] |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if cap and len(rows) > cap: |
| report = ut.limit_report(key, st=lent) or {} |
| limits[key] = report |
| problems.append( |
| f"{key}: {len(rows):,} rows exceeds the {cap:,}-row ceiling; refusing " |
| f"({report.get('cause', 'a truncated table understates every rollup it feeds')}). " |
| f"{report.get('recommendation', '')}".strip()) |
| |
| |
| |
| if cap == 0: |
| continue |
| stored = len(((existing.get(key) or {}).get("rows")) or {}) |
| if stored and len(rows) < stored * MAX_SHRINK: |
| problems.append( |
| f"{key}: the mirror answered {len(rows)} rows against {stored} stored β a drop of " |
| f"more than {int((1 - MAX_SHRINK) * 100)}% is a bad read, not Odoo history " |
| f"shrinking; refusing rather than deleting rows that still exist") |
| built["problems"] = problems |
| |
| |
| |
| |
| built["limits"] = limits |
| return built |
|
|
|
|
| def apply_plan(rt, built, username="automation", today=None): |
| """Create-or-merge every table in `built` and its rows. Idempotent by construction. |
| |
| Row ids ARE the Odoo ids, so a re-run updates in place and never appends a second copy of the |
| same record β which is also what makes "every Odoo unique id is in the database" a checkable |
| statement rather than a hopeful one. |
| |
| β ONLY THE BUCKETS PRESENT ARE WRITTEN, so a caller (or a gate) may hand in a subset. |
| """ |
| if built.get("problems"): |
| raise Refused("; ".join(built["problems"])) |
| stamp = today or _iso_today() |
| written = {} |
| plans = [(key, label, fields(), built[bucket]) |
| for bucket, key, label, fields in TABLES if bucket in built] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| def _apply_all(cur): |
| cur = cur if isinstance(cur, dict) else {} |
| for key, label, fields, rows in plans: |
| written[key] = _ensure_table_inplace(cur, key, label, fields, rows, username, stamp) |
| return cur |
|
|
| rt.update(_ut().STORE_KEY, _apply_all, flush="sync") |
| return written |
|
|
|
|
| def _ensure_table(rt, key, label, fields, rows, username, stamp): |
| """One table, written on its own. Kept because the gate drives a single table directly, and |
| because a caller with one table to reconcile should not have to compose an updater.""" |
| written = {} |
|
|
| def _one(cur): |
| cur = cur if isinstance(cur, dict) else {} |
| written["counts"] = _ensure_table_inplace(cur, key, label, fields, rows, username, stamp) |
| return cur |
|
|
| rt.update(_ut().STORE_KEY, _one, flush="sync") |
| return written["counts"] |
|
|
|
|
| def _ensure_table_inplace(cur, key, label, fields, rows, username, stamp): |
| """One table INSIDE a caller's updater: definition merged, rows reconciled, dict mutated. |
| |
| β ROWS THAT LEFT THE POPULATION ARE REMOVED, and that stayed correct through the widening β |
| but only because the populations widened to "everything Odoo has". While `ut_odoo_customers` |
| was built FROM open invoices, removal meant a customer who paid their bill vanished from the |
| registry; now a partner leaves only when their last document does. The shrink guard in |
| `plan()` is the backstop for the case this policy cannot distinguish: a partial mirror read. |
| """ |
| ut = _ut() |
| wanted = {r["_id"]: {k: v for k, v in r.items() if k != "_id"} for r in rows} |
| for row in wanted.values(): |
| row["refreshed"] = stamp |
| counts = {"added": 0, "updated": 0, "removed": 0, "rows": len(wanted)} |
| table = cur.get(key) |
| if table is None: |
| if len(cur) >= ut.MAX_TABLES: |
| |
| |
| raise Refused(f"{key}: tenant is at MAX_TABLES={ut.MAX_TABLES}; nothing created") |
| table = cur[key] = { |
| "key": key, "label": label, "source": ut.AUTOMATION_SOURCE, |
| "createdBy": username, "created": stamp, "fields": [], "rows": {}, |
| |
| |
| "recordMode": ut.AUTOMATION_RECORD_MODE, |
| } |
| table.setdefault("recordMode", ut.AUTOMATION_RECORD_MODE) |
| |
| |
| |
| |
| |
| if key in READ_THROUGH_KEYS: |
| table["readThrough"] = True |
| have = {str(f.get("key")): f for f in (table.get("fields") or [])} |
| for field in fields: |
| fkey = str(field.get("key")) |
| if fkey not in have: |
| table.setdefault("fields", []).append(dict(field)) |
| continue |
| |
| |
| |
| |
| |
| |
| stored = have[fkey] |
| |
| |
| |
| |
| if _ut().user_edited(stored): |
| continue |
| if (stored.get("automation") or {}).get("preset"): |
| for prop in ("label", "type", "options", "link", "rollup", "description", |
| "agg", "pinned", "default"): |
| if prop in field: |
| stored[prop] = field[prop] |
| else: |
| stored.pop(prop, None) |
| stored_rows = table.setdefault("rows", {}) |
| for rid, values in wanted.items(): |
| current = stored_rows.get(rid) |
| if current is None: |
| stored_rows[rid] = dict(values) |
| counts["added"] += 1 |
| elif any(str(current.get(k, "")) != str(v) for k, v in values.items() |
| if k != "refreshed"): |
| current.update(values) |
| counts["updated"] += 1 |
| else: |
| current["refreshed"] = values["refreshed"] |
| for rid in [r for r in stored_rows if r not in wanted]: |
| stored_rows.pop(rid, None) |
| counts["removed"] += 1 |
| return counts |
|
|
|
|
| def is_royal(tenant): |
| return str(tenant or "").strip().lower() in RI_SLUGS |
|
|
|
|
| def refresh(rt, tenant, username="automation", cur=None, today=None): |
| """THE entry point β the store-resync path and the route both call this. |
| |
| β It must be CALLED on resync by something outside this file. If it is not wired, every row |
| still carries a `refreshed` stamp, so a stale worklist is at least LEGIBLE rather than |
| silently authoritative. |
| """ |
| if not is_royal(tenant): |
| raise Refused(f"tenant {tenant!r} has no Odoo mirror behind these tables (R1: Royal " |
| f"Imports only); refusing to spawn empty locked databases") |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if rt is not None: |
| rt.assert_datastore_matches() |
| |
| |
| |
| |
| |
| |
| |
| if rt is not None and frozen(rt): |
| return {"tables": {}, "frozen": True, |
| "note": "this workspace has disconnected Odoo; its databases are frozen as " |
| "static data and are not being refreshed"} |
| if cur is None: |
| from harness import datastore |
| cur = datastore.ro_con() |
| built = plan(cur, rt=rt) |
| |
| |
| |
| |
| |
| |
| |
| |
| if rt is not None: |
| keep = enabled_buckets(rt) |
| skipped = sorted(key for bucket, key, _l, _f in TABLES if bucket not in keep) |
| for bucket, _key, _l, _f in TABLES: |
| if bucket not in keep: |
| built.pop(bucket, None) |
| else: |
| skipped = [] |
| written = apply_plan(rt, built, username=username, today=today) |
| return {"tables": written, |
| |
| **({"skipped": skipped} if skipped else {}), |
| **{bucket: len(built[bucket]) for bucket, _k, _l, _f in TABLES if bucket in built}} |
|
|