Skip to main content
Version: v1.0.0(int)

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

  1. Confirm the PO and capture context. PurchaseOrders.getPoById returns the PO. Capture targetType (materials vs finished goods, which determines which ledger is written), poNumber (used as ref on every ledger entry below), and shipToLocationId (the receiving location).

  2. Block terminal POs. If the PO is in Paid or Cancelled, refuse the receipt. A terminal PO cannot accept new movements.

  3. Create the receipt header. PurchaseOrders.createReceipt writes a row with the receipt date and notes. The per-line receipt items and ledger entries are added in the next step.

  4. Post each receipt line and its ledger entry. For every input line, add a po_receipt_items row via PurchaseOrders.addReceiptItem, then write one ledger entry: MaterialsInventory.recordReceipt for materials POs, FinishedGoodsInventory.recordReceipt for FG POs. Each ledger entry carries ref = poNumber, locationId = shipToLocationId, and unitCost from the receipt input (falling back to the PO line's unit cost when not specified).

  5. Roll up costs on the PO header. Recompute costs = sum(receipt_items.quantity × unit_cost) + shippingCosts + setupCosts and write it back to the PO header.

  6. 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 to Partial. Call PurchaseOrders.updatePo with the new status. This is a receipt-driven transition from any non-terminal source status; the manual-transition map does not apply.

  7. Cascade, refresh material FIFO layers. (Materials POs only.) Collect every materialId touched 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.

  8. Cascade, update work-order material costs. (Materials POs only.) For every work_order_items row whose SKU snapshot has an input referencing any of the affected materials, recompute material_unit_cost = Σ (snapshot_input.quantity × fifo_unit_cost_for_input(quantity)), where fifo_unit_cost_for_input returns the average cost of the oldest layers covering the requested quantity. Sub-assembly SKU inputs recurse through their own snapshot. Write the result via WorkOrders.setMaterialUnitCost.

  9. Cascade, update FG ledger costs. (Materials POs only.) For every affected work order, update the unit_cost on its ORDER, ALLOCATE, and PRODUCE / RECEIVE entries in the finished-goods ledger to material_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 Received rather than Partial. 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 Partial or Received in step 6 is computed from cumulative receipts and applied regardless of the source status, as long as the PO is not terminal (Paid / Cancelled).
  • costs is the persisted actual rollup. Step 5 keeps the PO header's costs field in sync with the receipts.

Errors

  • NotFoundError. The PO does not exist in this org.
  • InvalidTransitionError. The PO is in a terminal status (Paid or Cancelled).
  • ValidationError. A receipt line references a poItemId that doesn't belong to this PO.