toga-ai 1.0.652 → 1.0.653
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/2.0/apps/_underscore/INDEX.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/calculated-sql-fields.md +2 -2
- package/knowledge/2.0/apps/_underscore/features/netsuite-salesorder-address-phone-sync.md +14 -13
- package/knowledge/2.0/apps/_underscore/features/sales-order-po-number-sourcing.md +71 -8
- package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md +72 -7
- package/knowledge/2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md +3 -3
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/features/po-number-upstream-direction.md +37 -10
- package/package.json +1 -1
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
38
38
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
39
39
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
40
|
-
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
41
|
-
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php |
|
|
40
|
+
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
41
|
+
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php |
|
|
42
42
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
|
|
43
43
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
44
44
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Client/Language.php, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, toga25-supply/src/App.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/contexts/AuthContext.tsx, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Core/2026-08-21a - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Compass/2026-08-04a - ApprovalDetailsAssignedManagerPreferredStage.sql, _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
@@ -23,7 +23,7 @@ related:
|
|
|
23
23
|
- ../../api2/features/v2-api-error-codes.md
|
|
24
24
|
- ./model-magic-field-access.md
|
|
25
25
|
- ./model-save-parent-cascade-stored-field-deadlock.md
|
|
26
|
-
- ./
|
|
26
|
+
- ./sales-order-purchase-order-bridge-direction.md
|
|
27
27
|
---
|
|
28
28
|
|
|
29
29
|
## Summary
|
|
@@ -186,7 +186,7 @@ generous end; `Developer` and `Public` get no ACL row for any field.
|
|
|
186
186
|
`Client_Imhouston` (1 of 35 prod) and 7 of 36 client-sandbox schemas, driving the re-runnable
|
|
187
187
|
`Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql`. Recorded the `Base` + `API`
|
|
188
188
|
by-name ACL convention for these rows. Cross-linked
|
|
189
|
-
[PO-to-SO bridge tables](./
|
|
189
|
+
[PO-to-SO bridge tables](./sales-order-purchase-order-bridge-direction.md) — the metadata row makes
|
|
190
190
|
`_purchaseOrders` *requestable*, but the field's SQL was reading the wrong bridge table, so a
|
|
191
191
|
successful rollout still returned blank for most clients. (bala)
|
|
192
192
|
- 2026-08-18 - Documented **how a calculated field becomes requestable**: a client-scoped
|
|
@@ -16,7 +16,7 @@ related:
|
|
|
16
16
|
- acl-permission-chain.md
|
|
17
17
|
- address-uniqueness-normalization.md
|
|
18
18
|
- carrier-shipping-labels.md
|
|
19
|
-
-
|
|
19
|
+
- sales-order-purchase-order-bridge-direction.md
|
|
20
20
|
- ../../toga2-supply/features/fulfill-and-ship.md
|
|
21
21
|
- ../../api2/features/record-scripts.md
|
|
22
22
|
---
|
|
@@ -41,15 +41,16 @@ changes behaviour for all 22 clients at once**.
|
|
|
41
41
|
(and was NULL on the order tested), while the **bridge covers 12,643 of 12,658** orders. Resolve the
|
|
42
42
|
PO through the bridge, not the column.
|
|
43
43
|
|
|
44
|
-
**⚠ …and it is the
|
|
45
|
-
read.** This trait writes the PO link via the api2 child key **`purchaseOrderSalesOrders`**,
|
|
46
|
-
|
|
47
|
-
`_Model_Client_SalesOrder::_purchaseOrders()` queried **`SalesOrders_PurchaseOrders
|
|
48
|
-
table populated by a *different*
|
|
49
|
-
|
|
50
|
-
72,134 prod orders
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
**⚠ …and it is the UPSTREAM bridge, which the shared `SalesOrders._purchaseOrders` field did not
|
|
45
|
+
read.** This trait writes the PO link via the api2 child key **`purchaseOrderSalesOrders`**, landing
|
|
46
|
+
in **`PurchaseOrders_SalesOrders`** (upstream — the *customer's* PO). The base calculated field
|
|
47
|
+
`_Model_Client_SalesOrder::_purchaseOrders()` queried **`SalesOrders_PurchaseOrders`** (downstream),
|
|
48
|
+
a *different* table populated by a *different* pipeline — the 1.0
|
|
49
|
+
`App_Api_Toga2::syncPurchaseOrderFromNetsuite` cron — holding Agilant's **vendor** POs. So every
|
|
50
|
+
client on this trait stored a correct PO the field could not see: **41,381 of 72,134** prod orders
|
|
51
|
+
rendered blank until a downstream-then-upstream `COALESCE` was added. Read
|
|
52
|
+
[SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md) **before** touching either
|
|
53
|
+
side of this link — the child key you choose decides which table you write, silently.
|
|
53
54
|
|
|
54
55
|
## ⚠ Four traps when writing a field on this shared trait
|
|
55
56
|
|
|
@@ -102,11 +103,11 @@ intended, not a fault.
|
|
|
102
103
|
## Change history
|
|
103
104
|
|
|
104
105
|
- 2026-08-26 — Corrected the PO half of the "where the values come from" table: the
|
|
105
|
-
`purchaseOrderSalesOrders` child key this trait posts writes the **
|
|
106
|
+
`purchaseOrderSalesOrders` child key this trait posts writes the **upstream** bridge
|
|
106
107
|
(`PurchaseOrders_SalesOrders`, customer PO), while the shared `SalesOrders._purchaseOrders`
|
|
107
|
-
calculated field read the **
|
|
108
|
+
calculated field read the **downstream** bridge (`SalesOrders_PurchaseOrders`, Agilant vendor PO) — so
|
|
108
109
|
all 22 trait clients stored a PO the UI could not display (41,381 of 72,134 prod orders blank).
|
|
109
|
-
Cross-linked
|
|
110
|
+
Cross-linked [SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md). (bala)
|
|
110
111
|
- 2026-08-11 — TRUE-80824: created. Prefilled TOGa Supply's Reference 1 / Reference 2 / Phone # from
|
|
111
112
|
NetSuite by sourcing `SalesOrders.number`, the bridge-linked purchase order (`otherRefNum` — **not**
|
|
112
113
|
`customerPurchaseOrder`, ~59% populated vs the bridge's 12,643/12,658), and the shipping address's
|
|
@@ -6,11 +6,13 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
13
13
|
- _underscore/Model/Compass/SalesOrder.php
|
|
14
|
+
- _underscore/Model/Compass/Canada/SalesOrder.php
|
|
15
|
+
- dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql
|
|
14
16
|
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
15
17
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
|
|
16
18
|
- dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql
|
|
@@ -50,12 +52,53 @@ deliberately **not** set: the value is a scalar comma-joined string, not a perso
|
|
|
50
52
|
via `resolvePathInsensitive(valueKey)`, normalising an empty string to an em-dash.
|
|
51
53
|
|
|
52
54
|
### The tenant layer (`_purchaseOrders`)
|
|
55
|
+
|
|
56
|
+
**Only TWO classes in the whole tree define this method** — verified with
|
|
57
|
+
`grep -rn "function _purchaseOrders" _underscore/`:
|
|
58
|
+
|
|
53
59
|
| Tenant | Implementation | Direction read |
|
|
54
60
|
|---|---|---|
|
|
55
|
-
| **Compass USA** |
|
|
56
|
-
| **Quad Graphics** | no override → base implementation | downstream
|
|
57
|
-
|
|
|
58
|
-
|
|
61
|
+
| **Compass USA and Compass Canada** | both inherit `_Model_Compass_SalesOrder::_purchaseOrders()` (`_underscore/Model/Compass/SalesOrder.php:238`) — a `UNION ALL` of its own vendor POs plus Office Depot's POs traversed SO→PO→SO→PO across the multi-tier chain | downstream **+ upstream** |
|
|
62
|
+
| **Quad Graphics** | no override → base implementation | downstream, then upstream fallback |
|
|
63
|
+
| base (`_Model_Client_SalesOrder:349`) | the shared calculated field | **downstream, falling back to upstream** (see below) |
|
|
64
|
+
|
|
65
|
+
**⚠ Correction (2026-08-26): Compass Canada is not on the base implementation.** The earlier "no
|
|
66
|
+
`Model/Compasscanada/` directory" reading looked in the wrong place — Compass's tenants nest one
|
|
67
|
+
level deeper, at `_underscore/Model/Compass/Canada/SalesOrder.php`, where
|
|
68
|
+
`_Model_Compass_Canada_SalesOrder extends _Model_Compass_SalesOrder`. Compass USA is
|
|
69
|
+
`Model/Compass/Usa/SalesOrder.php` and does the same. So **any change to the Compass override moves
|
|
70
|
+
both Compass tenants**, and neither is affected by a change to the base. Compass Canada's upstream
|
|
71
|
+
bridge is in fact **empty** (0 rows; 735 of 788 orders resolve downstream) — it is downstream-only,
|
|
72
|
+
not upstream-only.
|
|
73
|
+
|
|
74
|
+
### The base field now falls back to upstream (`COALESCE`)
|
|
75
|
+
|
|
76
|
+
Because the trait-based NetSuite sync writes **upstream** while the base field read **downstream**,
|
|
77
|
+
41,381 of 72,134 prod orders across the 19 base-model tenants resolved no PO at all. The base
|
|
78
|
+
`_purchaseOrders()` is now a **`COALESCE` of two scalar subqueries** — downstream first, upstream as
|
|
79
|
+
the fallback — which fixes every upstream tenant at once and needed **no per-tenant override**.
|
|
80
|
+
|
|
81
|
+
Branch state as verified **2026-08-26** (do not assume this is live):
|
|
82
|
+
|
|
83
|
+
| Repo | Change | Where it lives |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `_underscore` | the `COALESCE` fallback | commit `4579e0e7` on `TRUE-81284`, merged into **`_beta`**, `_sandbox-client`, `_sandbox-dev`. **Not on `_production`.** |
|
|
86
|
+
| `dbchanges2` | `Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql` (the `CustomRecordFields` row for the schemas the 2026-08-11b rollout missed) | commit `0ca4012` on `TRUE-81284`, pushed. **Not on `_main` or `_beta`.** |
|
|
87
|
+
|
|
88
|
+
Verified while applied: **zero existing values changed for any tenant** — the fallback only fills
|
|
89
|
+
nulls — while coverage went NYCHH 64 → 819 of 819, Elite 57 → 288 of 288, Quad 475 → 1,456 of 1,463.
|
|
90
|
+
The MySQL 8 plan stayed indexed (both subqueries `ref` on `idx_salesOrderId`, then `eq_ref` on the
|
|
91
|
+
`PurchaseOrders` primary key) and `COALESCE` short-circuits, so the upstream subquery only runs when
|
|
92
|
+
the downstream one is null.
|
|
93
|
+
|
|
94
|
+
#### ⚠ Still open — which direction wins when an order has both
|
|
95
|
+
|
|
96
|
+
Downstream-first shows the **vendor** PO, while the sales-orders **list** (whose table-view join
|
|
97
|
+
reads upstream) shows the **customer** PO, so one order can display two different PO numbers in the
|
|
98
|
+
list and in the modal. Orders carrying both are real and not rare: NYCHH 64, Elite 55, Quad 469.
|
|
99
|
+
Downstream-first was chosen, then questioned in favour of treating the list as the source of truth;
|
|
100
|
+
a per-tenant override on `_Model_Nychh_SalesOrder` preferring upstream was offered and **not built**.
|
|
101
|
+
**Settle this before quoting either value as correct.**
|
|
59
102
|
|
|
60
103
|
### `poSelection` is a different thing
|
|
61
104
|
The `poSelection` config on the `sales-order-record-actions` surface picks **which** linked PO the
|
|
@@ -74,14 +117,34 @@ the two — changing one does not move the other.
|
|
|
74
117
|
- **Surface seed `id`s are deterministic; surface `uuid`s are not.** Seeds insert with `UUID()`, so
|
|
75
118
|
the element uuid differs per environment. A uuid quoted in an older migration will **not** match
|
|
76
119
|
the uuid in a live payload. Key surface updates on the literal `id` (here, `id = 7`).
|
|
77
|
-
- **Downstream-only base implementation
|
|
78
|
-
tenant `_purchaseOrders`
|
|
120
|
+
- **Downstream-only base implementation was a trap for upstream tenants.** On an upstream-only
|
|
121
|
+
tenant `_purchaseOrders` resolved cleanly (no `EV-8`) and returned `null` forever. Closed on
|
|
122
|
+
`_beta` by the `COALESCE` fallback above. See
|
|
79
123
|
[SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md) and
|
|
80
124
|
[NYCHH PO Number](../../../clients/nychh/features/po-number-upstream-direction.md).
|
|
125
|
+
- **A correct value here still may not reach the older TOGa Supply order-details modal.** That UI is
|
|
126
|
+
config-driven and declares the display row and the API field request **separately**: the
|
|
127
|
+
`DEFAULTFIELDS` and `QUAD` field sets ask for a `_purchaseOrders` row without listing
|
|
128
|
+
`_purchaseOrders` in `apiFields.fetchOrderDetails`, so it renders blank regardless of the SQL. See
|
|
129
|
+
[order-detail field config](../../toga2-supply/features/order-detail-field-config-and-customer-name.md).
|
|
130
|
+
- **Fixing the SQL is not enough on a tenant whose schema lacks the metadata row.** No
|
|
131
|
+
`CustomRecordFields` row (`recordId` 14, field `_purchaseOrders`) means `EV-8` no matter what the
|
|
132
|
+
model returns, and the original per-client rollout **missed schemas** — prod 1 of 35, sandbox 7 of
|
|
133
|
+
36. See [calculated SQL fields](./calculated-sql-fields.md).
|
|
81
134
|
- Dropping `isPersonaValue` is intentional; re-adding it will make the FE try to treat the string
|
|
82
135
|
as a persona array.
|
|
83
136
|
|
|
84
137
|
## Change history
|
|
138
|
+
- 2026-08-26 — Base `_purchaseOrders()` became a **`COALESCE`** of downstream-then-upstream, closing
|
|
139
|
+
the upstream-tenant gap for all base-model tenants with no per-tenant override (verified: no
|
|
140
|
+
existing value changed; NYCHH 64 → 819/819, Elite 57 → 288/288, Quad 475 → 1,456/1,463; plan stays
|
|
141
|
+
indexed). Recorded its branch state (`_beta`, **not** `_production`) and left the
|
|
142
|
+
**downstream-vs-upstream tie-break open** for orders carrying both (NYCHH 64, Elite 55, Quad 469),
|
|
143
|
+
where the modal and the list can disagree. **Corrected the tenant table**: Compass Canada is not on
|
|
144
|
+
the base implementation — it lives at `Model/Compass/Canada/` and inherits the Compass override, so
|
|
145
|
+
both Compass tenants move together and Compass Canada's upstream bridge is empty. Added the two
|
|
146
|
+
other independent causes of a blank PO (missing `CustomRecordFields` row → `EV-8`; the older supply
|
|
147
|
+
order-details field set never requesting the field). (bala)
|
|
85
148
|
- 2026-08-25 — Repointed `SurfaceElements` id 7 to `_purchaseOrders` for all clients
|
|
86
149
|
(`Core/2026-08-25a`, reverting `Core/2026-07-23a`) and made the FE adapter honour the surface
|
|
87
150
|
`valueKey` instead of a hardcoded path; recorded the corrected per-tenant map, which supersedes
|
package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
CHANGED
|
@@ -6,17 +6,20 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/PurchaseOrders/SalesOrder.php
|
|
13
13
|
- _underscore/Model/Client/SalesOrders/PurchaseOrder.php
|
|
14
14
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
15
15
|
- _underscore/Model/Client/SalesOrder.php
|
|
16
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
17
|
+
- library/app/api/toga2.php
|
|
16
18
|
related:
|
|
17
19
|
- ./sales-order-po-number-sourcing.md
|
|
18
20
|
- ./recursive-item-fulfillments.md
|
|
19
21
|
- ./fulfillable-item-propagation.md
|
|
22
|
+
- ./netsuite-salesorder-address-phone-sync.md
|
|
20
23
|
- ../../api2/features/v2-rest-query-contract.md
|
|
21
24
|
- ../../../clients/nychh/features/po-number-upstream-direction.md
|
|
22
25
|
---
|
|
@@ -47,10 +50,45 @@ origin; the second is what was generated from it.
|
|
|
47
50
|
- A multi-tier tenant (Compass USA) has **both**, chained SO→PO→SO→PO.
|
|
48
51
|
- The only in-code statement of the distinction today is a docblock at
|
|
49
52
|
`_underscore/Model/Client/ItemFulfillment.php:136-140`.
|
|
50
|
-
- The base `_purchaseOrders` calculated field on `_Model_Client_SalesOrder` (line ~348)
|
|
51
|
-
**downstream** table only. See
|
|
53
|
+
- The base `_purchaseOrders` calculated field on `_Model_Client_SalesOrder` (line ~348) read the
|
|
54
|
+
**downstream** table only until 2026-08-25, when a **fallback to upstream** was added. See
|
|
52
55
|
[sales-order PO Number sourcing](./sales-order-po-number-sourcing.md).
|
|
53
56
|
|
|
57
|
+
### Two writers, one per direction — and the child key is what picks the table
|
|
58
|
+
|
|
59
|
+
The two directions are not two views of one write; they are populated by **two different sync
|
|
60
|
+
pipelines**, in **different frameworks**:
|
|
61
|
+
|
|
62
|
+
| | UPSTREAM (`PurchaseOrders_SalesOrders`) | DOWNSTREAM (`SalesOrders_PurchaseOrders`) |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Written by | **2.0** `_Trait_Netsuite_SalesOrder` (`_underscore/Trait/Netsuite/SalesOrder.php` ~:664-680 and ~:720) | **1.0** cron `App_Api_Toga2::syncPurchaseOrderFromNetsuite` (`library/app/api/toga2.php:2028`, posts the child key at :2504) |
|
|
65
|
+
| api2 child key | `purchaseOrderSalesOrders` | `salesOrderPurchaseOrders` |
|
|
66
|
+
| PO number stored | NetSuite `$nsOrder->otherRefNum` — the **customer's** PO (`EIT0011108`, `LOI-Woodhull`, `GAVELL/08252026`, Quad `4992713`) | the **vendor** PO — numeric NetSuite PO numbers (`168984`), Elite's `P100019` |
|
|
67
|
+
|
|
68
|
+
**⚠ The child key is the only thing that decides which table you write, and getting it wrong is
|
|
69
|
+
silent.** api2 treats `salesOrderPurchaseOrders` and `purchaseOrderSalesOrders` as two unrelated
|
|
70
|
+
child collections, so a transposed key writes a perfectly valid row into the *other* direction and
|
|
71
|
+
the reading side simply never sees it. No error, no warning. Read the key aloud, like the routes.
|
|
72
|
+
|
|
73
|
+
Because **22 client `SalesOrder` models compose `_Trait_Netsuite_SalesOrder`**, every NetSuite-synced
|
|
74
|
+
tenant writes **upstream** rows — which is why upstream-only tenants are the norm on that trait
|
|
75
|
+
rather than the exception. (Grep the composition list before assuming a trait change is
|
|
76
|
+
client-scoped; note the same grep also matches the `SalesOrderItem` / `SalesOrderStage` models, so
|
|
77
|
+
count `SalesOrder.php` files only or you will report 66.)
|
|
78
|
+
|
|
79
|
+
### How much downstream-only cost, measured on prod
|
|
80
|
+
|
|
81
|
+
Because the base field read downstream only while the trait wrote upstream, **41,381 of 72,134**
|
|
82
|
+
sales orders across the 19 base-model tenants returned no PO at all — only 30,753 resolved. Worst
|
|
83
|
+
affected: NYCHH 64/822, Canon 594/6,290, GroWrk 1,226/12,793, S&P Global 971/9,468, Endeavor Health
|
|
84
|
+
1,919/11,836. **An order can also carry both directions at once** (NYCHH 64, Elite 55, Quad 469),
|
|
85
|
+
which is what makes the tie-break in
|
|
86
|
+
[PO Number sourcing](./sales-order-po-number-sourcing.md) a real decision rather than a formality.
|
|
87
|
+
|
|
88
|
+
All **35** prod client schemas contain all four tables (`SalesOrders`, `PurchaseOrders`, both
|
|
89
|
+
bridges), verified via `information_schema` — so reading either direction is structurally safe
|
|
90
|
+
everywhere. Present is not populated.
|
|
91
|
+
|
|
54
92
|
## Gotchas
|
|
55
93
|
|
|
56
94
|
- **An empty array is the symptom.** `200` / `totalRecordCount 0` with **no** `EZ-*` or `EV-*`
|
|
@@ -62,11 +100,38 @@ origin; the second is what was generated from it.
|
|
|
62
100
|
- **The routes are near-homographs.** `sales-order-purchase-orders` vs
|
|
63
101
|
`purchase-order-sales-orders`. Read them aloud before wiring a front-end call.
|
|
64
102
|
- **Downstream-only assumptions are baked into shared code.** Any tenant whose POs are purely
|
|
65
|
-
upstream (verified: NYCHH
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
upstream (verified: NYCHH) silently gets `null`/empty from the shared downstream-reading helpers
|
|
104
|
+
rather than an error.
|
|
105
|
+
- **⚠ Compass Canada is NOT upstream-only, and it DOES have model overrides** — corrected
|
|
106
|
+
2026-08-26. The earlier "no `Model/Compasscanada/` directory" reading looked in the wrong place:
|
|
107
|
+
the folder is **`_underscore/Model/Compass/Canada/`**, and
|
|
108
|
+
`_Model_Compass_Canada_SalesOrder extends _Model_Compass_SalesOrder`, so Compass Canada inherits
|
|
109
|
+
**Compass's** `_purchaseOrders` override, not the base one. Its **upstream bridge is completely
|
|
110
|
+
empty (0 rows)** while 735 of 788 orders resolve a PO downstream. When checking whether a tenant
|
|
111
|
+
has overrides, search for the class name (`grep -rn "class _Model_.*_SalesOrder extends"`) rather
|
|
112
|
+
than guessing a directory — Compass's tenants nest one level deeper than everyone else's.
|
|
113
|
+
- **⚠ Do NOT paper over the direction with an `ojoin` on a shared single-record fetch.** It is
|
|
114
|
+
tempting because it works in one place: the TOGa Supply NYCHH orders **list** joins the upstream
|
|
115
|
+
bridge and is correct — but only because NYCHH's upstream bridge happens to be **1:1** (823 rows /
|
|
116
|
+
823 distinct sales orders, max 1 PO per SO). The same join in the shared
|
|
117
|
+
`fetchOrdersDetails` (`toga2-supply/src/pages/Orders/api/OrdersApi.ts:397`) breaks three tenants:
|
|
118
|
+
**Compass** has up to **6,440** downstream POs on one sales order (row multiplication on a
|
|
119
|
+
single-record fetch), **Compass Canada's upstream bridge is empty** (735 of 788 would drop to
|
|
120
|
+
**zero**), and **Prudential** has only **1,868 of 26,843** orders upstream (92% populated would
|
|
121
|
+
become 7%). A scalar `GROUP_CONCAT` calculated field is the right mechanism precisely because it
|
|
122
|
+
collapses many POs without multiplying rows.
|
|
68
123
|
|
|
69
124
|
## Change history
|
|
125
|
+
- 2026-08-26 — Added the **write** side: the two directions are populated by two different
|
|
126
|
+
pipelines (2.0 `_Trait_Netsuite_SalesOrder` writes **upstream** customer POs from `otherRefNum`;
|
|
127
|
+
the 1.0 `syncPurchaseOrderFromNetsuite` cron writes **downstream** vendor POs), and the api2 child
|
|
128
|
+
key (`purchaseOrderSalesOrders` vs `salesOrderPurchaseOrders`) is the only thing selecting the
|
|
129
|
+
table — a transposed key writes the wrong direction silently. Quantified the downstream-only cost
|
|
130
|
+
on prod (41,381 of 72,134 orders returned no PO across the 19 base-model tenants) and noted
|
|
131
|
+
tenants carrying both directions. **Corrected the Compass Canada claim**: it has overrides at
|
|
132
|
+
`Model/Compass/Canada/` and inherits the Compass `_purchaseOrders` override, and its upstream
|
|
133
|
+
bridge is empty — it is downstream-only, not upstream-only. Added the "do not fix this with an
|
|
134
|
+
`ojoin`" gotcha with the Compass / Compass Canada / Prudential regressions it would cause. (bala)
|
|
70
135
|
- 2026-08-25 — Documented the two-direction bridge after an NYCHH "empty array" debug that was
|
|
71
136
|
purely a wrong-direction query; recorded route/model/record-id mapping and the empty-vs-EZ
|
|
72
137
|
diagnostic signature (apeterson)
|
package/knowledge/2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md
CHANGED
|
@@ -18,7 +18,7 @@ related:
|
|
|
18
18
|
- ../../../../clients/elite/features/supply2-scope.md
|
|
19
19
|
- ../workflows/client-host-scoping.md
|
|
20
20
|
- ../architecture.md
|
|
21
|
-
- ../../_underscore/features/
|
|
21
|
+
- ../../_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
## Summary
|
|
@@ -109,7 +109,7 @@ As of 2026-08-26 that still reports `1` for `DEFAULTFIELDS.json` and the three `
|
|
|
109
109
|
fix was drafted this session but is not present on any branch or stash.** The backend half (the
|
|
110
110
|
`_purchaseOrders` calculated field reading the wrong bridge table) is a separate, independent cause
|
|
111
111
|
of the same blank row; see
|
|
112
|
-
[PO-to-SO bridge tables](../../_underscore/features/
|
|
112
|
+
[PO-to-SO bridge tables](../../_underscore/features/sales-order-purchase-order-bridge-direction.md).
|
|
113
113
|
|
|
114
114
|
### Which field set a host gets
|
|
115
115
|
|
|
@@ -164,7 +164,7 @@ requires **both** changes, not either one:
|
|
|
164
164
|
covers COMPASS / COMPASSCANADA / QUAD (+ an ELITE early return), so **every other host falls
|
|
165
165
|
through to `DEFAULTFIELDS`** — a default-set gap is a platform-wide gap. The four-file fix was
|
|
166
166
|
drafted but is **not on any branch**; the backend half is a separate cause, see
|
|
167
|
-
[PO-to-SO bridge tables](../../_underscore/features/
|
|
167
|
+
[PO-to-SO bridge tables](../../_underscore/features/sales-order-purchase-order-bridge-direction.md). (bala)
|
|
168
168
|
- 2026-08-18 — First capture. Documented that the order-details Customer name is **hardcoded in the
|
|
169
169
|
frontend**, not read from the database: every host's config uses
|
|
170
170
|
`valueKey: "orderBoxDetails.customer.name"`, `orderBoxDetails` exists nowhere in the codebase or
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql |
|
|
5
|
+
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
6
6
|
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql |
|
|
@@ -6,12 +6,14 @@ project: _Underscore
|
|
|
6
6
|
client: nychh
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/SalesOrder.php
|
|
13
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
13
14
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
|
|
14
15
|
- dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
|
|
16
|
+
- dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql
|
|
15
17
|
related:
|
|
16
18
|
- ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
17
19
|
- ../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
@@ -22,7 +24,13 @@ related:
|
|
|
22
24
|
|
|
23
25
|
NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live
|
|
24
26
|
in the **upstream** table `PurchaseOrders_SalesOrders` (route `purchase-order-sales-orders`,
|
|
25
|
-
record 276).
|
|
27
|
+
record 276). Their POs are the customer's own numbers — `EIT0011108`, `COR0015555`,
|
|
28
|
+
`LOI-Woodhull` — written from NetSuite `otherRefNum` by `_Trait_Netsuite_SalesOrder`.
|
|
29
|
+
|
|
30
|
+
**Refinement (prod, 2026-08-26): "no downstream rows" holds for the vast majority but not all.** Of
|
|
31
|
+
822 NYCHH sales orders, **64 carry a PO in BOTH directions**, which is exactly the set where the
|
|
32
|
+
list and the modal can disagree — see the open tie-break below. On prod the downstream-only base
|
|
33
|
+
field resolved a PO on only **64 of 822** orders.
|
|
26
34
|
|
|
27
35
|
This produced a symptom that looked like a bug and was not: the sales-order modal's PO Number came
|
|
28
36
|
back as an empty array while the sales-orders **list** column showed a PO number for the same
|
|
@@ -44,16 +52,35 @@ the **upstream** table.
|
|
|
44
52
|
|
|
45
53
|
## Gotchas
|
|
46
54
|
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
- ✅ **The open consequence is addressed on `_beta` — and NOT with a per-tenant override.** The
|
|
56
|
+
shared `_purchaseOrders` field read the **downstream** table only
|
|
57
|
+
(`_underscore/Model/Client/SalesOrder.php:349`), so once the modal bound to `_purchaseOrders`
|
|
58
|
+
NYCHH resolved it without an `EV-8` and got `null`. Rather than a NYCHH-specific field, the
|
|
59
|
+
**base** field became a `COALESCE` of downstream-then-upstream, which fixed every upstream tenant
|
|
60
|
+
at once: NYCHH went from 64 to **819 of 819** orders resolving a PO, with **no existing value
|
|
61
|
+
changed** for any tenant. It sits on `_beta` / the sandbox branches, **not `_production`** — see
|
|
62
|
+
[PO Number sourcing](../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md) for
|
|
63
|
+
the exact commits. NYCHH is also not the only upstream tenant: all 22 clients on
|
|
64
|
+
`_Trait_Netsuite_SalesOrder` write upstream.
|
|
65
|
+
- 🚨 **Still open for NYCHH specifically: the 64 both-direction orders.** Downstream-first makes the
|
|
66
|
+
modal show the **vendor** PO while the list keeps showing the **customer** PO. A
|
|
67
|
+
`_Model_Nychh_SalesOrder` override preferring upstream was offered and not built. Decide which one
|
|
68
|
+
NYCHH should see before treating either surface as authoritative.
|
|
69
|
+
- **The NYCHH orders list is not a template.** Its table-view join through the upstream bridge is
|
|
70
|
+
safe **only** because NYCHH's upstream bridge is coincidentally 1:1 (823 rows / 823 distinct sales
|
|
71
|
+
orders). Copying that join into a shared single-record fetch breaks Compass (up to 6,440 POs on one
|
|
72
|
+
order), Compass Canada (empty upstream bridge) and Prudential (1,868 of 26,843 upstream).
|
|
53
73
|
- Do not "fix" this by adding downstream rows. The direction is correct for NYCHH's business model;
|
|
54
|
-
the shared field is what
|
|
74
|
+
the shared field is what was incomplete.
|
|
55
75
|
|
|
56
76
|
## Change history
|
|
77
|
+
- 2026-08-26 — Closed the open `_purchaseOrders` gap: fixed in the **base** field as a
|
|
78
|
+
downstream-then-upstream `COALESCE` (not a NYCHH override), taking NYCHH from 64 to **819 of 819**
|
|
79
|
+
orders with a PO and changing no existing value; currently on `_beta`, not `_production`. Refined
|
|
80
|
+
"no downstream rows" — **64 of 822** NYCHH orders have both directions, which is the remaining open
|
|
81
|
+
question (modal would show the vendor PO, the list shows the customer PO). Recorded that the NYCHH
|
|
82
|
+
orders-list upstream join is safe only because their upstream bridge is 1:1 and must not be copied
|
|
83
|
+
into a shared fetch. (bala)
|
|
57
84
|
- 2026-08-25 — Diagnosed the empty-array PO Number as a wrong-direction query (upstream data,
|
|
58
85
|
downstream route); ruled out ACL and migrations; recorded the still-open `_purchaseOrders`
|
|
59
86
|
downstream-only gap for this tenant (apeterson)
|
package/package.json
CHANGED