W122: Create receipt against purchase order
Records a delivery against an open purchase order. Triggered from the Purchase Order Detail receipts section when goods arrive from the vendor. The workflow writes the PO-side receipt rows, posts the matching entries to the materials or finished-goods inventory ledger, advances the PO's status, and, for materials POs, propagates the receipt's unit costs downstream through every work order that consumes the affected materials.
Steps
-
Confirm the PO and capture context.
PurchaseOrders.getPoByIdreturns the PO. CapturetargetType(materials vs finished goods, which determines which ledger is written),poNumber(used asrefon every ledger entry below), andshipToLocationId(the receiving location). -
Block terminal POs. If the PO is in
PaidorCancelled, refuse the receipt. A terminal PO cannot accept new movements. -
Create the receipt header.
PurchaseOrders.createReceiptwrites a row with the receipt date and notes. The per-line receipt items and ledger entries are added in the next step. -
Post each receipt line and its ledger entry. For every input line, add a
po_receipt_itemsrow viaPurchaseOrders.addReceiptItem, then write one ledger entry:MaterialsInventory.recordReceiptfor materials POs,FinishedGoodsInventory.recordReceiptfor FG POs. Each ledger entry carriesref = poNumber,locationId = shipToLocationId, andunitCostfrom the receipt input (falling back to the PO line's unit cost when not specified). -
Roll up
costson the PO header. Recomputecosts = sum(receipt_items.quantity × unit_cost) + shippingCosts + setupCostsand write it back to the PO header. -
Advance the PO status. Compare cumulative received quantities (this receipt plus any prior ones) against each line's expected quantity. If every line is fully received, transition to
Received; otherwise transition toPartial. CallPurchaseOrders.updatePowith the new status. This is a receipt-driven transition from any non-terminal source status; the manual-transition map does not apply. -
Cascade, refresh material FIFO layers. (Materials POs only.) Collect every
materialIdtouched by this receipt. The new RECEIVE entries become FIFO layers (oldest-first) for that material at the receiving location. There is no aggregate to recompute; FIFO consumption reads the layers in order on demand. The recompute below is what actually changes downstream. -
Cascade, update work-order material costs. (Materials POs only.) For every
work_order_itemsrow whose SKU snapshot has an input referencing any of the affected materials, recomputematerial_unit_cost = Σ (snapshot_input.quantity × fifo_unit_cost_for_input(quantity)), wherefifo_unit_cost_for_inputreturns the average cost of the oldest layers covering the requested quantity. Sub-assembly SKU inputs recurse through their own snapshot. Write the result viaWorkOrders.setMaterialUnitCost. -
Cascade, update FG ledger costs. (Materials POs only.) For every affected work order, update the
unit_coston its ORDER, ALLOCATE, and PRODUCE / RECEIVE entries in the finished-goods ledger tomaterial_unit_cost + conversion_cost, keyed by(ref = wo_number, sku_id). CONSUME entries on shipped SOs are not touched (they were priced at shipment time).
Returns
The new receipt header with its received-item rows hydrated.
Business rules
- Over-receipt is allowed. Received quantities may exceed expected quantities; the PO simply lands in
Receivedrather thanPartial. Quantities must be non-negative. - Atomic across the cascade. Steps 3 through 9 run inside a single transaction. A reader of the ledger will never see RECEIVE entries without the downstream cost rollup, status advance, and cost cascade also applied.
- FG POs skip the materials cascade. Finished-goods receipts have no downstream cost dependents (no WO is built on a finished-goods PO line). Steps 7 through 9 are inapplicable when
targetType = 'finished_goods'. - Receipt-driven transitions. The transition to
PartialorReceivedin step 6 is computed from cumulative receipts and applied regardless of the source status, as long as the PO is not terminal (Paid/Cancelled). costsis the persisted actual rollup. Step 5 keeps the PO header'scostsfield in sync with the receipts.
Errors
NotFoundError. The PO does not exist in this org.InvalidTransitionError. The PO is in a terminal status (PaidorCancelled).ValidationError. A receipt line references apoItemIdthat doesn't belong to this PO.