Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/procurement/CHARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 0 additions & 4 deletions packages/procurement/src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`,
Expand All @@ -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)`,
Expand All @@ -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
Expand All @@ -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)`,
Expand Down
1 change: 0 additions & 1 deletion packages/procurement/src/flows/pr_to_po_convert.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions packages/procurement/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
12 changes: 8 additions & 4 deletions packages/procurement/src/objects/procurement_order.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
14 changes: 8 additions & 6 deletions packages/procurement/src/objects/procurement_receipt.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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' }),

Expand Down
Loading