diff --git a/packages/procurement/CHARTER.md b/packages/procurement/CHARTER.md index 5f0c00e..164a309 100644 --- a/packages/procurement/CHARTER.md +++ b/packages/procurement/CHARTER.md @@ -28,7 +28,7 @@ portals — those remain pluggable. | State-machine driven lifecycle | PR `draft→submitted→approved→converted` | | Threshold-based approval routing | `pr_approval_required.flow` fires at ≥ $5k | | Auto-record-creation flow | `pr_to_po_convert.flow` drafts the PO | -| 2-way match signal | PO `match_status` formula (awaiting / partial / matched) from `received_amount` vs `total_amount`. `received_amount` is a stored header field maintained at the top level (client/seed) — a cross-object rollup hook from receipts would re-enter the QuickJS sandbox and crash, so it is NOT done in-hook (see `src/hooks/index.ts`). Full PO↔receipt↔invoice 3-way match is a fork. | +| 2-way match signal | PO `match_status` formula (awaiting / partial / matched) from `received_amount` vs `total_amount`. `received_amount` is a live `summary` roll-up — the engine sums `procurement_receipt.received_value` (over the `purchase_order` lookup) on every receipt change; rejected receipts carry value 0, so the sum equals the accepted total. (Earlier this was a hand-maintained stored field because a receipt→PO rollup hook crashed the sandbox — framework#1867, now fixed.) Full PO↔receipt↔invoice 3-way match is a fork. | | Scheduler-driven alerts | `po_overdue.flow` flags missed deliveries | | Internationalization | English + 简体中文 ship out of the box | | Realistic seed data | 4 vendors, 5 PRs, 4 POs, 3 receipts covering all states | diff --git a/packages/procurement/src/data/index.ts b/packages/procurement/src/data/index.ts index 2e3f12f..a344202 100644 --- a/packages/procurement/src/data/index.ts +++ b/packages/procurement/src/data/index.ts @@ -133,7 +133,6 @@ const orders = defineSeed(PurchaseOrder, { vendor: 'Cloudwell Hosting, Inc.', status: 'sent', total_amount: 180000, - received_amount: 0, payment_terms: 'net_30', order_date: cel`daysAgo(5)`, expected_delivery: cel`daysFromNow(25)`, @@ -144,7 +143,6 @@ const orders = defineSeed(PurchaseOrder, { vendor: 'Dell Technologies', status: 'partial', total_amount: 7500, - received_amount: 3750, payment_terms: 'net_45', order_date: cel`daysAgo(20)`, expected_delivery: cel`daysFromNow(5)`, @@ -155,7 +153,6 @@ const orders = defineSeed(PurchaseOrder, { vendor: 'Sato Translations K.K.', status: 'sent', total_amount: 3200, - received_amount: 0, payment_terms: 'net_45', order_date: cel`daysAgo(10)`, expected_delivery: cel`daysAgo(2)`, // OVERDUE → po_overdue flow @@ -166,7 +163,6 @@ const orders = defineSeed(PurchaseOrder, { vendor: 'Bright Marketing Studio', status: 'closed', total_amount: 18000, - received_amount: 18000, payment_terms: 'net_30', order_date: cel`daysAgo(120)`, expected_delivery: cel`daysAgo(90)`, diff --git a/packages/procurement/src/flows/pr_to_po_convert.flow.ts b/packages/procurement/src/flows/pr_to_po_convert.flow.ts index 0354c29..bb24447 100644 --- a/packages/procurement/src/flows/pr_to_po_convert.flow.ts +++ b/packages/procurement/src/flows/pr_to_po_convert.flow.ts @@ -55,7 +55,6 @@ export const PRToPOConvertFlow: Flow = { status: 'draft', owner: '{pr.requester}', total_amount: '{pr.estimated_amount}', - received_amount: 0, cost_center: '{pr.cost_center}', // order_date is intentionally left unset on the draft PO — the buyer // sets it when sending (enforced by `sent_requires_order_date`). A diff --git a/packages/procurement/src/hooks/index.ts b/packages/procurement/src/hooks/index.ts index 4a1817d..2d1f547 100644 --- a/packages/procurement/src/hooks/index.ts +++ b/packages/procurement/src/hooks/index.ts @@ -3,11 +3,10 @@ import { requestHook } from '../objects/procurement_request.hook'; import { orderHook } from '../objects/procurement_order.hook'; -// NOTE: `procurement_order.received_amount` is a STORED header field, not a -// hook-maintained rollup. A receipt → PO rollup would require a nested engine -// write from a hook, which is unsupported in the standalone runtime (the -// QuickJS hook sandbox crashes on nested writes; `ctx.services.data` is -// undefined inside it). See packages/expense/CHARTER.md. Maintain the total at -// the top level (client/seed) or, in a fork, via a native summary field / -// external worker. +// NOTE: `procurement_order.received_amount` is a live `summary` roll-up (sum of +// `procurement_receipt.received_value` over the `purchase_order` lookup), which +// the engine recomputes on every receipt insert/update/delete — no hook needed. +// Rejected receipts carry `received_value` 0, so a plain sum equals the accepted +// total. (Before framework#1867 was fixed, a receipt→PO rollup hook crashed the +// sandbox, so the total was hand-maintained; that workaround is gone.) export const allHooks = [requestHook, orderHook]; diff --git a/packages/procurement/src/objects/procurement_order.object.ts b/packages/procurement/src/objects/procurement_order.object.ts index 2403ee9..6ae6e9d 100644 --- a/packages/procurement/src/objects/procurement_order.object.ts +++ b/packages/procurement/src/objects/procurement_order.object.ts @@ -69,13 +69,17 @@ export const PurchaseOrder = ObjectSchema.create({ group: 'commercial', min: 0, }), - received_amount: Field.currency({ + received_amount: Field.summary({ label: 'Received Amount', group: 'commercial', - min: 0, - defaultValue: 0, + summaryOperations: { + object: 'procurement_receipt', + field: 'received_value', + function: 'sum', + relationshipField: 'purchase_order', + }, description: - 'Sum of accepted goods-receipt values. Stored header field — maintained at the top level (client/seed), not by a cross-object rollup hook (unsupported in the standalone runtime; see procurement/src/hooks/index.ts).', + 'Sum of accepted goods-receipt values — a live roll-up the engine recomputes on every procurement_receipt insert/update/delete. Rejected receipts carry received_value 0, so a plain sum equals the accepted total. Previously a hand-maintained stored field because a nested-write rollup hook crashed the sandbox (framework#1867, now fixed).', }), payment_terms: Field.select({ label: 'Payment Terms', diff --git a/packages/procurement/src/objects/procurement_receipt.object.ts b/packages/procurement/src/objects/procurement_receipt.object.ts index 1105497..226139d 100644 --- a/packages/procurement/src/objects/procurement_receipt.object.ts +++ b/packages/procurement/src/objects/procurement_receipt.object.ts @@ -4,12 +4,14 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; import { P, F } from '@objectstack/spec'; /** - * Goods Receipt — one record per receiving event against a PO. The - * `received_amount` rollup on PurchaseOrder is updated by the - * `procurement_receipt.hook.ts` after-create handler. + * Goods Receipt — one record per receiving event against a PO. Its + * `received_value` rolls up into `procurement_order.received_amount` via a + * native `summary` field (the engine recomputes the PO total on every receipt + * change — no hook). * - * Quality outcome ("accepted" / "rejected" / "partial") drives the 3-way - * match: only `accepted` value rolls up into PO.received_amount. + * Quality outcome ("accepted" / "rejected" / "partial") drives the 3-way match: + * a rejected receipt carries `received_value` 0 (enforced below), so only + * accepted value contributes to the PO's summed `received_amount`. */ export const GoodsReceipt = ObjectSchema.create({ name: 'procurement_receipt', @@ -48,7 +50,7 @@ export const GoodsReceipt = ObjectSchema.create({ required: true, min: 0, description: - 'Dollar value of goods accepted in this receipt. Rolls up into PO.received_amount via the after-create hook.', + 'Dollar value of goods accepted in this receipt. Summed into PO.received_amount by a native summary field (0 for rejected receipts).', }), notes: Field.markdown({ label: 'Notes' }),