toga-ai 1.0.56 → 1.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/netsuite-suiteql-rest-shim.md +117 -0
- package/knowledge/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +111 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/item-fulfillment-tracking-tableview.md +57 -18
- package/package.json +1 -1
|
@@ -4,3 +4,4 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
|
|
6
6
|
| [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
|
|
7
|
+
| [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NetSuite SuiteQL/REST Shim — Field Semantics
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-11
|
|
10
|
+
owners: [dfranks]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/api/netsuite/rest.php
|
|
13
|
+
related:
|
|
14
|
+
- ../architecture.md
|
|
15
|
+
- ../../worker/features/forecast2-netsuite-reconciliation.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
`App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP
|
|
21
|
+
toolkit. Its `list*()` methods return **shim objects whose PHP class/shape matches the old
|
|
22
|
+
SOAP records**, so existing cron code keeps working. This doc captures the **non-obvious
|
|
23
|
+
SuiteQL/REST field semantics** discovered while building and reconciling the Forecast2 sync —
|
|
24
|
+
the things that silently produce wrong totals or duplicate rows if you don't know them.
|
|
25
|
+
|
|
26
|
+
Production NetSuite code must use REST (SuiteQL/REST), not SOAP — SOAP is being phased out and
|
|
27
|
+
is OK only for ad-hoc troubleshooting.
|
|
28
|
+
|
|
29
|
+
## Key files / entry points
|
|
30
|
+
|
|
31
|
+
- `library/app/api/netsuite/rest.php`
|
|
32
|
+
- `listSales($from, $to, $entityIds, $dateField)` — Invoice/CashSale/CashRfnd/CustCred.
|
|
33
|
+
- `listSalesOrders($from, $to, $entityIds)` — SalesOrd.
|
|
34
|
+
- Both do a SuiteQL header query, then (historically) a **per-id REST GET** per record for
|
|
35
|
+
fields SuiteQL doesn't expose on `transaction`, then a chunked `transactionline` query.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
Header SuiteQL → per-record detail → line SuiteQL, assembled into SOAP-shaped shims. Sign and
|
|
40
|
+
line-number conventions below are applied so the shim matches what SOAP returned and what the
|
|
41
|
+
Forecast2 tables already store.
|
|
42
|
+
|
|
43
|
+
## Field semantics / gotchas (verified on the live account)
|
|
44
|
+
|
|
45
|
+
- **Line number = `transactionline.id`, NOT `linesequencenumber`.** `tl.id` equals the REST
|
|
46
|
+
`item[].line` value (the stable SOAP-era line key); `linesequencenumber` is physical order and
|
|
47
|
+
re-maps/collides when SOAP-era records are edited. Verified: sales orders 106 orders / 344
|
|
48
|
+
lines (2019–2029, incl. edited orders with line-id gaps), invoices + cash sales 62/62.
|
|
49
|
+
`listSales` historically resolved line via `uniquekey → REST lineUniqueKey → line`; `tl.id`
|
|
50
|
+
gives the same answer in bulk without the per-id GET.
|
|
51
|
+
|
|
52
|
+
- **ShipItem lines have `costestimate = NULL`** (100% of 42K+ lines, 2025–26). In any profit
|
|
53
|
+
expression `SUM(-foreignamount + costestimate)`, a NULL cost makes the whole term NULL and SQL
|
|
54
|
+
**drops the row from the SUM** — so shipping *revenue* still counts but shipping *profit* does
|
|
55
|
+
not, silently understating NS profit. Always `NVL(tl.costestimate, 0)`. (This caused a fake
|
|
56
|
+
$663K "gap" in the audit tools — see the reconciliation doc.)
|
|
57
|
+
|
|
58
|
+
- **Cash refunds & credit memos carry extra COGS/inventory lines.** SuiteQL `transactionline`
|
|
59
|
+
with `mainline='F' AND item>0` returns, per item, the customer line **plus** a `CUSTOMERRETURN`
|
|
60
|
+
and an `ASSET` posting (all `item>0`), which REST's `item[].items` sublist omits. They are
|
|
61
|
+
distinguished by **`tl.iscogs = 'T'`** (and `accountinglinetype` IN `CUSTOMERRETURN`/`ASSET`).
|
|
62
|
+
Filter **`tl.iscogs = 'F'`** to get exactly the customer-facing lines. Invoices/cash sales do
|
|
63
|
+
not have this (0 extra lines). Without the filter, bulk imports duplicate refund/memo lines.
|
|
64
|
+
|
|
65
|
+
- **Sign conventions are per-type and self-consistent.** SuiteQL returns
|
|
66
|
+
`foreignamount`/`quantity`/`costestimate` **negative for invoices & cash sales, positive for
|
|
67
|
+
credit memos & cash refunds**. A uniform `-foreignamount` therefore yields **positive** revenue
|
|
68
|
+
for invoices and **negative** for reversals — the agreed Forecast2 convention. Do NOT also apply
|
|
69
|
+
a per-type factor (it double-negates reversals back to positive). The shim flips
|
|
70
|
+
`amount = -foreignamount`, `costEstimate = -costestimate`.
|
|
71
|
+
|
|
72
|
+
- **`createdFrom` is not a usable SuiteQL column.** `transaction.createdfrom` is **always NULL**
|
|
73
|
+
in SuiteQL (26/26). It is recoverable from `previoustransactionlinelink.previousdoc` excluding
|
|
74
|
+
`previoustype = 'Opprtnty'` (an invoice links to both its SO and its opportunity; REST's
|
|
75
|
+
`createdFrom` is the SO). **But this is non-deterministic for multi-link transactions** (credit
|
|
76
|
+
memos return several non-Opprtnty `previousdoc` rows in arbitrary order) — two runs can pick
|
|
77
|
+
different values. REST `createdFrom` (per-id GET) is the only deterministic source. Treat the
|
|
78
|
+
link table as best-effort, not authoritative.
|
|
79
|
+
|
|
80
|
+
- **AR open balance = `transaction.foreignamountunpaid`** (== REST `amountRemaining`, 12/12 on
|
|
81
|
+
open invoices). It is **NULL on fully-paid invoices and on cash sales/refunds** — wrap in
|
|
82
|
+
`NVL(...,0)` for a "settled = 0.00" convention. Valid balance columns are **`foreignamountunpaid`,
|
|
83
|
+
`foreignamountpaid`, `foreigntotal`** only; `amountremaining` and `amountunpaid` do **not exist**
|
|
84
|
+
and 400 the query.
|
|
85
|
+
|
|
86
|
+
- **SO originating line** lives in `previoustransactionlinelink` (correlated subquery on
|
|
87
|
+
`previoustype='SalesOrd'`), because `transactionline` has **no `orderline` column**.
|
|
88
|
+
|
|
89
|
+
- **TO_DATE rejects impossible dates** (e.g. `2026-06-31`) with an opaque HTTP 400 — validate
|
|
90
|
+
calendar dates before building SuiteQL.
|
|
91
|
+
|
|
92
|
+
## Data model
|
|
93
|
+
|
|
94
|
+
Reads NetSuite `transaction`, `transactionline`, `previoustransactionlinelink` via SuiteQL and
|
|
95
|
+
`/record/v1/{type}/{id}` via REST. Writes nothing.
|
|
96
|
+
|
|
97
|
+
## Client variations
|
|
98
|
+
|
|
99
|
+
None — uniform across clients (NetSuite is a single shared account).
|
|
100
|
+
|
|
101
|
+
## Gotchas / known issues
|
|
102
|
+
|
|
103
|
+
- The shim must stay PHP 7.2-compatible (library is 7.2 prod): no arrow functions, typed props,
|
|
104
|
+
`??=`, or `match`. Lint with `C:\xampp7\php\php.exe -l` before deploying.
|
|
105
|
+
- Field availability varies by NetSuite account **and** record type — probe the live account
|
|
106
|
+
before assuming a column/relationship exists.
|
|
107
|
+
|
|
108
|
+
## Change history
|
|
109
|
+
|
|
110
|
+
- 2026-06-11 — Documented bulk SuiteQL field semantics (tl.id==line, ShipItem NULL cost, iscogs
|
|
111
|
+
COGS filter, createdFrom non-determinism, foreignamountunpaid, sign conventions) surfaced while
|
|
112
|
+
moving the Forecast2 trueup tools off per-id REST GETs. (dfranks)
|
|
113
|
+
|
|
114
|
+
## Related docs
|
|
115
|
+
|
|
116
|
+
- `../architecture.md` — Library core architecture.
|
|
117
|
+
- `../../worker/features/forecast2-netsuite-reconciliation.md` — the tooling that applies these.
|
|
@@ -3,3 +3,4 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
|
|
6
|
+
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/reconcile_netsuite_totals.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-11
|
|
10
|
+
owners: [dfranks]
|
|
11
|
+
files:
|
|
12
|
+
- test/@dave/reconcile_netsuite_totals.php
|
|
13
|
+
- test/@dave/analyze_netsuite_forecast_diff.php
|
|
14
|
+
- test/@dave/trueup_sales.php
|
|
15
|
+
- test/@dave/trueup_open_orders.php
|
|
16
|
+
- worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../../library/features/netsuite-suiteql-rest-shim.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and
|
|
25
|
+
NetSuite. The 5-min importer (`common_import_sales_from_netsuite.php`) filters on
|
|
26
|
+
`lastmodifieddate` and the nightly discrepancy-fix only looks back 30 days, so older rows whose
|
|
27
|
+
NetSuite values change (e.g. cost re-valuation) are never re-pulled. These tools close that gap
|
|
28
|
+
by reconciling a chosen tranDate range directly against NetSuite.
|
|
29
|
+
|
|
30
|
+
## Key files / entry points
|
|
31
|
+
|
|
32
|
+
- `reconcile_netsuite_totals.php [from] [to]` — category grand totals NS vs Forecast2 (Sales,
|
|
33
|
+
Sales Profit, Open Orders, Opportunities) with deltas. Read-only. Connects explicitly to the
|
|
34
|
+
**prod core2 reader**; NetSuite via SuiteQL SUMs.
|
|
35
|
+
- `analyze_netsuite_forecast_diff.php [from] [to]` — decomposes the delta **per transaction** into
|
|
36
|
+
NS_ONLY (missing from FC), FC_ONLY (stale/extra), DRIFT (value differs). Read-only.
|
|
37
|
+
- `trueup_sales.php --from --to [--chunk-days N] [--prod] [--dry-run]` — makes `Forecast.Sales`
|
|
38
|
+
match NetSuite for a tranDate range (insert/update/delete per line).
|
|
39
|
+
- `trueup_open_orders.php --from= --to= [--prod] [--dry-run] [--verbose]` — same for
|
|
40
|
+
`Forecast.OpenOrderItems` (currently-open SOs whose tranDate falls in range).
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
- **Bulk SuiteQL (not per-id GET).** Both trueup tools were rewritten to fetch detail via chunked
|
|
45
|
+
`WHERE transaction IN (...)` SuiteQL using `tl.id` as the line number (see the shim doc for why
|
|
46
|
+
that's valid), replacing the ~1.5s-per-record REST GET. Result: a day that took ~48 min now
|
|
47
|
+
runs in seconds (parity verified — `trueup_sales` matched the old `listSales` path 1813/1813
|
|
48
|
+
records, 3675/3675 line cells on a sample day). `trueup_open_orders` builds its header+lines
|
|
49
|
+
per chunk; `trueup_sales` does it in a local `fetchSalesBulk()` that returns the same shim shape
|
|
50
|
+
`App_Api_Netsuite_Rest::listSales()` produced, so the reconciliation loop is unchanged.
|
|
51
|
+
- **`--prod` targeting (self-contained).** trueup writes via `App_Database(...,'db_forecast2')`.
|
|
52
|
+
On dev laptops that registry config points at **localhost XAMPP**; `--prod` overrides it in
|
|
53
|
+
memory at runtime by reading `[database_forecast2]` from `worker/config.worker.ini` (the core2
|
|
54
|
+
**writer**). `App_Database::registerDatabaseConnect` reads the registry config lazily on first
|
|
55
|
+
query, so the override also covers the keepalive reconnect. **Do not edit any `config.*.ini`** to
|
|
56
|
+
target prod — that mutates shared dev state and silently leaves the laptop pointed at the writer.
|
|
57
|
+
- **Chunking + keepalive.** trueup_sales chunks by `--chunk-days` (default 7) and commits per
|
|
58
|
+
chunk so a crash resumes. Both ping/reconnect `db_forecast2` before DB work (Aurora drops idle
|
|
59
|
+
links during long NS calls).
|
|
60
|
+
|
|
61
|
+
## Data model
|
|
62
|
+
|
|
63
|
+
`Forecast.Sales`, `Forecast.OpenOrderItems` on the **core2** cluster
|
|
64
|
+
(reader `reader1.core.database.togahub.com`, writer `writer.core.database.togahub.com`). Source
|
|
65
|
+
of truth is NetSuite; FC is made to match.
|
|
66
|
+
|
|
67
|
+
## Client variations
|
|
68
|
+
|
|
69
|
+
None — Forecast2 is a single shared dataset.
|
|
70
|
+
|
|
71
|
+
## Gotchas / known issues
|
|
72
|
+
|
|
73
|
+
- **NVL the cost in NS profit SUMs.** `reconcile`/`analyze` compute NS profit as
|
|
74
|
+
`SUM(-foreignamount + NVL(costestimate,0))`. Without `NVL`, ShipItem lines (NULL cost) drop from
|
|
75
|
+
the SUM and NS profit is understated, producing a **fake FC-over-NS "gap"** ($663K across 2025).
|
|
76
|
+
After the fix, the real residual is the opposite sign and small: **~$87K NS-over-FC**,
|
|
77
|
+
concentrated **Dec 2025–Apr 2026**, driven by NetSuite re-valuing `costestimate` on older
|
|
78
|
+
invoices that no `lastmodifieddate`-based sync re-pulls. (Revenue reconciles to the penny; only
|
|
79
|
+
profit drifts.)
|
|
80
|
+
- **Compare money at 2 decimals.** DB columns store 2dp but PHP `revenue - cost` carries float
|
|
81
|
+
dust (`313.6` vs `313.60000001`); raw `!=` produced thousands of phantom UPDATEs that re-wrote
|
|
82
|
+
identical values (1,839 on one open-orders run). Both tools now compare `round((float)$x, 2)`.
|
|
83
|
+
- **`trueup_sales` does not manage `createdFrom` or `amountDue`.**
|
|
84
|
+
- `createdFrom` — non-deterministic from the link table (see shim doc); the REST-based importer
|
|
85
|
+
owns it. trueup preserves it on UPDATE, leaves NULL on INSERT.
|
|
86
|
+
- `amountDue` (`Forecast.Sales`) and `dtPendingBilling` (`OpenOrderItems`) are **not yet in
|
|
87
|
+
prod** — those columns exist only in local dev (TRUE-78923 / TRUE-79081 migrations are
|
|
88
|
+
local-only). Both trueup tools have those fields **removed** so they run against prod. ⚠ The
|
|
89
|
+
prod migrations (`dbchanges2/Forecast/2026-06-09 *.sql`) must be applied **before** this
|
|
90
|
+
session's importer/discrepancy-fix changes (which write those columns) deploy, or production
|
|
91
|
+
crashes with `Unknown column`.
|
|
92
|
+
- **Always dry-run prod first** and confirm the change counts are real drift, not artifacts —
|
|
93
|
+
e.g. 2023-01-03 showed 240 "updates" that were entirely createdFrom backfill + profit float-dust
|
|
94
|
+
(zero real drift); after the fixes it correctly reports `unchanged=240`.
|
|
95
|
+
- These tools live in `test/@dave/` (developer tooling), but `trueup_open_orders` has been run
|
|
96
|
+
against production. The `Defaults`/checkpoint mechanics of the scheduled sync are separate.
|
|
97
|
+
|
|
98
|
+
## Change history
|
|
99
|
+
|
|
100
|
+
- 2026-06-11 — `trueup_sales` rewritten to bulk SuiteQL (`fetchSalesBulk`, ~300× faster);
|
|
101
|
+
`amountDue` + `createdFrom` dropped; float-dust fixed; `--prod` flag added. `reconcile`/`analyze`
|
|
102
|
+
profit queries `NVL(costestimate,0)` — established the $663K gap was an audit artifact and the
|
|
103
|
+
real drift is ~$87K NS-over-FC in Dec 2025–Apr 2026. (dfranks)
|
|
104
|
+
- 2026-06-10 — `trueup_open_orders` bulk SuiteQL refactor + `--prod`; phantom float-dust UPDATEs
|
|
105
|
+
eliminated; Aurora idle-drop reconnect. (dfranks)
|
|
106
|
+
|
|
107
|
+
## Related docs
|
|
108
|
+
|
|
109
|
+
- `../../library/features/netsuite-suiteql-rest-shim.md` — the SuiteQL/REST field semantics these
|
|
110
|
+
tools rely on.
|
|
111
|
+
- `../architecture.md` — Worker architecture (forecast2 sync lives in `crons/toga2/forecast2/`).
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,8 +4,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
8
|
-
- **worker** (Worker) —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 3 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
|
+
- **worker** (Worker) — 3 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
|
|
10
10
|
## 2.0 framework
|
|
11
11
|
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql |
|
|
6
|
-
| [Compass: Item
|
|
6
|
+
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql |
|
|
7
7
|
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php |
|
|
8
8
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
@@ -1,46 +1,85 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Compass: Item
|
|
2
|
+
title: "Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)"
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
project: _Underscore
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-06-
|
|
8
|
+
updated: 2026-06-11
|
|
9
9
|
owners: ["jcardinal"]
|
|
10
10
|
files:
|
|
11
11
|
- dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql
|
|
12
|
+
- dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql
|
|
12
13
|
related:
|
|
13
14
|
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
## Summary
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
**Units** with INNER joins (so non-serialized items with no unit never appeared) and sourced the
|
|
21
|
-
outbound tracking number from the now-dropped `ItemFulfillmentItemUnits.trackingNumberId`.
|
|
19
|
+
Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply,
|
|
20
|
+
both driven by `TableViews` / `TableViewJoins` / `TableViewFields` config (no PHP):
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
- **`item-fulfillments-for-sales-order-items`** (`TableViews.id = 13`) — fulfilled items for a
|
|
23
|
+
single sales-order line.
|
|
24
|
+
- **`item-fulfillments-for-sales-orders`** (`TableViews.id = 12`) — fulfilled items across the
|
|
25
|
+
whole sales order.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Both were originally **rooted at `Units` (record 31) with INNER joins upward**, so any fulfilled
|
|
28
|
+
item with no serialized unit (no `ItemFulfillmentItemUnits` / `Units`) never appeared — and for
|
|
29
|
+
view 12, an entire order with zero unit-level rows returned **0 records** even though its items
|
|
30
|
+
were fulfilled. View 13 was re-rooted on 2026-06-10; view 12 on 2026-06-11.
|
|
31
|
+
|
|
32
|
+
## How it works (new definitions)
|
|
33
|
+
|
|
34
|
+
Both views are re-rooted at **`ItemFulfillmentItems` (record 29)** so every fulfilled item appears,
|
|
35
|
+
with the unit/tracking chain made **OUTER** so missing units no longer drop rows.
|
|
36
|
+
|
|
37
|
+
**View 13 — `item-fulfillments-for-sales-order-items`:**
|
|
38
|
+
- INNER `SalesOrderItems`; OUTER `ItemFulfillmentItemUnits` → OUTER `Units` → OUTER
|
|
39
|
+
`ItemFulfillmentItemUnits_TrackingNumbers` (record 319) → OUTER `TrackingNumbers` → OUTER `ShippingCarriers`.
|
|
40
|
+
- Columns: **Sales Order Line Number, Quantity Fulfilled (`ItemFulfillmentItems.quantity`),
|
|
30
41
|
Outbound Tracking #, Carrier, Serial #, Asset Tag.**
|
|
31
|
-
|
|
42
|
+
|
|
43
|
+
**View 12 — `item-fulfillments-for-sales-orders`:**
|
|
44
|
+
- Same base + OUTER unit/tracking chain, plus the order-scoping INNER joins:
|
|
45
|
+
INNER `SalesOrderItems`, INNER `ItemFulfillments`, INNER `SalesOrders`, INNER `Items`.
|
|
46
|
+
- Columns: **Sales Order Line Number, Part Number (`Items.partNumber`), Quantity Fulfilled,
|
|
47
|
+
Outbound Tracking # (hyperlink via `TrackingNumbers._hyperlink` rf 1272), Carrier, Serial #, Asset Tag.**
|
|
48
|
+
- Default sort re-pointed to the line-number field.
|
|
49
|
+
|
|
50
|
+
Stable Core ids used: records 29 IFI, 30 IFIU, 31 Units, 15 SalesOrderItems, 14 SalesOrders,
|
|
51
|
+
28 ItemFulfillments, 21 Items, 319 IFIU-tracking bridge, 62 TrackingNumbers, 9 ShippingCarriers.
|
|
52
|
+
|
|
53
|
+
## Data model
|
|
54
|
+
|
|
55
|
+
Join keys (RecordField ids): 60 SOI.id / 368 IFI.salesOrderItemId; 360 IF.id / 367 IFI.itemFulfillmentId;
|
|
56
|
+
59 SO.id / 254 SOI.salesOrderId; 105 Item.id / 66 SOI.itemId; 374 IFIU.itemFulfillmentItemId / 365 IFI.id;
|
|
57
|
+
236 Unit.id / 375 IFIU.unitId; 2183 bridge.itemFulfillmentItemUnitId / 372 IFIU.id;
|
|
58
|
+
336 TN.id / 2184 bridge.trackingNumberId; 21 SC.id / 613 TN.shippingCarrierId.
|
|
32
59
|
|
|
33
60
|
## Gotchas / known issues
|
|
34
61
|
|
|
62
|
+
- **A Unit-rooted view returns 0 rows whenever the order has no unit-level breakdown.** Symptom
|
|
63
|
+
seen on SA132740 (salesOrderId 105826): 13 SalesOrderItems, 7 ItemFulfillmentItems, but **0
|
|
64
|
+
ItemFulfillmentItemUnits / 0 Units** → view 12 showed "0 records" while the re-rooted sibling
|
|
65
|
+
view 13 showed the items fine. Fix is structural (re-root + OUTER chain), independent of the
|
|
66
|
+
missing-unit data issue.
|
|
35
67
|
- Tracking is sourced through the bridge join (`ItemFulfillmentItemUnits_TrackingNumbers.trackingNumberId`),
|
|
36
|
-
NOT the dropped column. The bridge record/fields
|
|
37
|
-
|
|
38
|
-
- `TableViews.sortPrimaryTableViewFieldId` is a FK to `TableViewFields`; it
|
|
39
|
-
old fields
|
|
68
|
+
NOT the dropped `ItemFulfillmentItemUnits.trackingNumberId` column. The bridge record/fields
|
|
69
|
+
(319 / 2183 / 2184) come from the Core migration and must exist before these files run.
|
|
70
|
+
- `TableViews.sortPrimaryTableViewFieldId` is a FK to `TableViewFields`; set it NULL before deleting
|
|
71
|
+
the old fields, then re-point via a multi-table UPDATE (not a self-subquery) to avoid MySQL 1093.
|
|
40
72
|
- `TableViewFields` must be deleted before `TableViewJoins` (`tableViewJoinId` FK).
|
|
73
|
+
- With null `joinOnTableViewJoinId`, the engine resolves a join's parent table by the `recordId` of
|
|
74
|
+
`parentRecordFieldId` — this only works because each record appears once in the graph. Reusing a
|
|
75
|
+
record twice in one view would require an explicit `joinOnTableViewJoinId`.
|
|
41
76
|
|
|
42
77
|
## Client variations
|
|
43
|
-
|
|
78
|
+
Compass-only. The underlying bridge model is shared (see the _underscore feature doc).
|
|
79
|
+
|
|
80
|
+
## Change history
|
|
81
|
+
- 2026-06-11 — Re-rooted `item-fulfillments-for-sales-orders` (id 12) from Units to ItemFulfillmentItems with OUTER unit/tracking chain; fixes 0-records on orders lacking unit breakdown (e.g. SA132740). (jcardinal)
|
|
82
|
+
- 2026-06-10 — Re-rooted `item-fulfillments-for-sales-order-items` (id 13) and moved tracking to the bridge join. (jcardinal)
|
|
44
83
|
|
|
45
84
|
## Related docs
|
|
46
85
|
- 2.0 _underscore: Tracking-Number Bridge Migration.
|
package/package.json
CHANGED