Complete documentation of PO processing from creation through vendor acknowledgment, goods receipt, invoicing, and closure. Includes revision management, schedule lines, and completion tracking.
A Purchase Order (PO) is the legal document sent to vendors to procure goods or services. It flows through internal approval, vendor communication, receipt tracking, and invoice matching before closure.
| Status | Description | Allowed Actions | Next States |
|---|---|---|---|
| DRAFT | PO being prepared. Not yet submitted. | Edit, Delete, Submit | PENDING_APPROVAL |
| PENDING_APPROVAL | Submitted for approval workflow. | Approve, Reject, Recall | APPROVED, REJECTED, DRAFT |
| APPROVED | Approved. Ready to send to vendor. | Send to Vendor, Revise, Cancel | SENT, DRAFT (revise), CANCELLED |
| SENT | Transmitted to vendor via email/portal. | Record Acknowledgment | ACKNOWLEDGED |
| ACKNOWLEDGED | Vendor confirmed receipt and acceptance. | Create GRN, Revise | PARTIAL, RECEIVED |
| PARTIAL | Some items received, more pending. | Create GRN, Close Manually | RECEIVED, CLOSED |
| RECEIVED | All ordered quantities received. | Process Invoice | CLOSED |
| CLOSED | Fully received and invoiced. Terminal. | View Only | - |
| CANCELLED | Cancelled. No further action. | View Only | - |
Created via RFQ award. PR → RFQ → Bid → PO. Most common type. Full approval workflow applies based on amount.
PR → PO without RFQ. Used for single-source items, low-value purchases, or when vendor is pre-determined. po_items.pr_item_id links directly to PR.
Direct PO without PR or RFQ. Used for urgent one-off purchases. No pr_item_id or rfq_item_id. Requires justification.
Pre-approved PO for recurring needs. Releases can be made against it without re-approval up to the blanket limit.
PO issued against an existing rate contract. Pricing and terms pulled from contract. Tracks utilization.
Urgent purchase. May have expedited or post-facto approval. Requires justification and audit trail.
| Path | po_type | po_rfqs | po_items.pr_item_id | po_items.rfq_item_id |
|---|---|---|---|---|
| PR → RFQ → PO | STANDARD | Populated | Via rfq_item → pr_item | Populated |
| PR → PO (skip RFQ) | DIRECT | Empty | Populated | NULL |
| Direct PO (no PR, no RFQ) | SPOT | Empty | NULL | NULL |
| Rate Contract → PO | CONTRACT_RELEASE | Empty | Optional | NULL |
id - UUID primary keypo_number - Unique PO number (auto-generated)company_id, vendor_id - Key relationshipsquotation_id, contract_id - Source documents (RFQs linked via po_rfqs junction table)po_date, required_date, expected_delivery_datepo_type - STANDARD (via RFQ), DIRECT (PR→PO, no RFQ), SPOT (no PR/RFQ), BLANKET, CONTRACT_RELEASE, EMERGENCYbuyer_id - Purchasing agentvendor_contact_* - Vendor snapshot at PO timeship_to_*, bill_to_* - Addressespayment_terms_id, delivery_terms_idsubtotal, tax_amount, total_amountcurrency_code, exchange_ratestatus, current_approval_levelrevision, revised_at, revision_reasontotal_received_qty, total_invoiced_amountis_fully_received, is_fully_invoicedpo_id, line_number - Parent and sequencepr_item_id, quotation_item_id - Source tracingitem_id, item_code, item_descriptionquantity, uom_idreceived_qty, invoiced_qty, cancelled_qtyunit_price, discount_*, tax_*, line_totalrequired_date, promised_dategl_account_id, cost_center_id, project_idwarehouse_id - Destination warehousestatus - OPEN, PARTIAL, RECEIVED, CANCELLED, CLOSEDEnables multiple RFQ awards to be consolidated into a single PO for the same vendor, reducing admin overhead and shipping costs.
id - Primary keypo_id - FK to purchase_ordersrfq_id - FK to rfqs (source RFQ)awarded_amount - Amount awarded from this RFQadded_by - User who consolidated the RFQ into POadded_at - Timestampremarks - Notes on consolidationpo_item_id, schedule_numberquantity - Scheduled delivery quantitydelivery_date - Expected delivery datereceived_qty - Quantity received against this scheduleship_to_location_id - Specific delivery locationstatus - SCHEDULED, PARTIAL, RECEIVED, CANCELLEDpo_id, approval_levelapprover_id, delegate_idaction - APPROVED, REJECTED, RETURNEDaction_date, commentsstatus, due_date, reminded_atStandard and custom terms and conditions attached to purchase orders. Supports both reusable company-wide terms and PO-specific custom terms.
id - UUID primary keypo_id - FK to purchase_ordersterm_type - STANDARD, CUSTOMterm_category - PAYMENT, DELIVERY, WARRANTY, PENALTY, LIABILITY, OTHERtitle - Term headingdescription - Full term textsequence - Display orderis_mandatory - Whether term is requiredtemplate_id - FK to standard term template (if STANDARD type)created_by, created_atTracks line-item-level changes within a PO amendment, capturing before/after values for quantity, price, dates, and specifications.
id - UUID primary keyamendment_id - FK to po_amendmentspo_item_id - FK to po_itemschange_type - ADD, MODIFY, CANCELfield_name - Column that changed (quantity, unit_price, delivery_date, etc.)old_value, new_value - Before/after snapshotsold_quantity, new_quantity - Quantity change detailold_unit_price, new_unit_price - Price change detailold_delivery_date, new_delivery_date - Date change detailreason - Justification for the item-level changeApproval workflow specific to PO amendments. May follow different approval rules than original PO (e.g., value-increase triggers higher-level approval).
id - UUID primary keyamendment_id - FK to po_amendmentsapproval_level - Level in the approval hierarchyapprover_id - FK to users (assigned approver)delegate_id - FK to users (delegated approver, if any)action - APPROVED, REJECTED, RETURNEDaction_date, commentsstatus - PENDING, COMPLETED, SKIPPEDdue_date, reminded_atCaptures vendor accept/reject decisions on PO amendments. Vendors may accept fully, reject, or propose counter-terms.
id - UUID primary keyamendment_id - FK to po_amendmentsvendor_id - FK to vendorsresponse - ACCEPTED, REJECTED, COUNTER_PROPOSEDresponse_date - When vendor respondedrespondent_name, respondent_email - Vendor contact who respondedcomments - Vendor remarks or counter-proposal detailscounter_proposal - JSON/text of proposed alternative termsacknowledged_by, acknowledged_at - Internal acknowledgment of responseImmutable audit trail for all amendment-related changes. Records every state transition, field change, and user action for compliance.
id - UUID primary keyamendment_id - FK to po_amendmentspo_id - FK to purchase_ordersaction - CREATED, SUBMITTED, APPROVED, REJECTED, VENDOR_ACCEPTED, VENDOR_REJECTED, APPLIED, CANCELLEDperformed_by - FK to usersperformed_at - Timestamp of actionold_values - JSONB snapshot of previous statenew_values - JSONB snapshot of new stateip_address, user_agent - Request metadatacomments - Contextual notesTracks vendor acknowledgment of PO receipt. Supports multiple acknowledgment methods (portal, email, manual) and captures vendor confirmation details.
id - UUID primary keypo_id - FK to purchase_ordersvendor_id - FK to vendorsacknowledged_at - Timestamp of acknowledgmentacknowledgment_method - PORTAL, EMAIL, MANUAL, APIvendor_reference - Vendor's internal reference numberrespondent_name, respondent_email - Vendor contactconfirmed_delivery_date - Vendor-confirmed delivery datecomments - Vendor remarks or conditionsrevision - PO revision number being acknowledgedrecorded_by - User who recorded (for manual entries)File attachments associated with purchase orders, such as specifications, drawings, terms documents, or vendor correspondence.
id - UUID primary keypo_id - FK to purchase_ordersfile_name - Original file namefile_path - Storage path or URLfile_size - Size in bytesmime_type - File MIME typeattachment_type - SPECIFICATION, DRAWING, TERMS, CORRESPONDENCE, OTHERdescription - Brief description of the attachmentuploaded_by - FK to usersuploaded_at - Timestampis_vendor_visible - Whether attachment is shared with vendorCompany and plant addresses used for ship-to and bill-to on purchase orders. Supports multiple address types per company location.
id - UUID primary keycompany_id - FK to companiesaddress_type - SHIPPING, BILLING, PLANT, WAREHOUSE, HEAD_OFFICEaddress_name - Label/name for the addressaddress_line_1, address_line_2city, state, postal_code, countrycontact_person, contact_phone, contact_emailis_default - Default address for the typeis_active - Soft-delete flagcreated_at, updated_atRecords PO closure and short-close events. Tracks whether closure was automatic (fully received/invoiced) or manual with justification.
id - UUID primary keypo_id - FK to purchase_ordersclosure_type - AUTO, MANUAL, SHORT_CLOSE, FORCE_CLOSEclosure_reason - Justification text (required for manual/short-close)closed_by - FK to usersclosed_at - Closure timestamptotal_ordered_value - Original PO value at closuretotal_received_value - Value of goods receivedtotal_invoiced_value - Value invoiced at closurevariance_amount - Difference between ordered and received/invoicedapproved_by, approved_at - Closure approval (for manual closures)Line-item-level closure details capturing outstanding quantities, short-close quantities, and per-item closure reasons.
id - UUID primary keyclosure_id - FK to po_closurespo_item_id - FK to po_itemsordered_qty - Original ordered quantityreceived_qty - Quantity received at closureinvoiced_qty - Quantity invoiced at closureshort_close_qty - Quantity being short-closed (not to be delivered)cancelled_qty - Previously cancelled quantityitem_closure_reason - Per-item justificationfinal_status - RECEIVED, SHORT_CLOSED, CANCELLEDLinks purchase orders to their source RFQs, enabling consolidation of multiple RFQ awards into a single PO for the same vendor.
id - UUID primary keypo_id - FK to purchase_ordersrfq_id - FK to rfqs (source RFQ)awarded_amount - Amount awarded from this RFQadded_by - User who consolidated the RFQ into POadded_at - Timestampremarks - Notes on consolidationMulti-level PO approval workflow supporting delegation, escalation, and configurable approval hierarchies based on PO value and type.
id - UUID primary keypo_id - FK to purchase_ordersapproval_level - Level in the approval hierarchyapprover_id - FK to users (assigned approver)delegate_id - FK to users (delegated approver)action - APPROVED, REJECTED, RETURNEDaction_date, commentsstatus - PENDING, COMPLETED, SKIPPEDdue_date, reminded_atescalated_to - FK to users (if escalated)escalated_at - Escalation timestampDelivery schedule lines for PO items, enabling split deliveries across multiple dates and locations for a single line item.
id - UUID primary keypo_item_id - FK to po_itemsschedule_number - Sequence within the itemquantity - Scheduled delivery quantitydelivery_date - Expected delivery datereceived_qty - Quantity received against this scheduleship_to_location_id - FK to locations (specific delivery point)status - SCHEDULED, PARTIAL, RECEIVED, CANCELLEDgrn_reference - Link to GRN that fulfilled this scheduleremarks - Delivery notesPOs support revisions after approval to handle changes in quantities, pricing, delivery dates, or specifications. Each revision is tracked for audit purposes.
Revisions are needed for: quantity changes, price adjustments, delivery date changes, line item additions/cancellations, or specification updates after the PO has been sent to the vendor.
The revision process increments the version number and may require re-approval depending on the nature of changes (e.g., value increase).
-- Create revision UPDATE po.purchase_orders SET revision = revision + 1, revised_at = CURRENT_TIMESTAMP, revised_by = :user_id, revision_reason = :reason, status = CASE WHEN :value_increased THEN 'PENDING_APPROVAL' ELSE 'APPROVED' END WHERE id = :po_id;
Revised PO is re-sent to vendor with revision number clearly indicated. Vendor acknowledgment is tracked again for the new revision.
Value increases typically require re-approval. Quantity reductions or delivery date changes may not. Configure thresholds in the approval matrix.
For items with multiple delivery dates or locations, schedule lines allow splitting a single PO line into multiple deliveries:
-- Example: 1000 units split across 4 deliveries -- PO Item: 1000 units of Item XYZ INSERT INTO po.po_schedule_lines (po_item_id, schedule_number, quantity, delivery_date) VALUES (:po_item_id, 1, 250, '2024-02-01'), (:po_item_id, 2, 250, '2024-02-15'), (:po_item_id, 3, 250, '2024-03-01'), (:po_item_id, 4, 250, '2024-03-15');
Track partial deliveries against specific schedules. Monitor on-time delivery performance. Support just-in-time inventory management.
PO completion is tracked at both header and line item level:
po_items.received_qty - Updated from GRNspurchase_orders.total_received_qty - Sumis_fully_received - All items received-- Check if fully received UPDATE po.purchase_orders SET is_fully_received = ( SELECT BOOL_AND( received_qty >= quantity - cancelled_qty ) FROM po.po_items WHERE po_id = :po_id ) WHERE id = :po_id;
po_items.invoiced_qty - From invoicespurchase_orders.total_invoiced_amountis_fully_invoiced - All items invoiced-- Check if fully invoiced UPDATE po.purchase_orders SET is_fully_invoiced = ( total_invoiced_amount >= total_amount ) WHERE id = :po_id;
A PO automatically closes when: (1) is_fully_received = true AND is_fully_invoiced = true, OR (2) manually closed by authorized user with justification. Closure prevents further GRNs or invoices against the PO.
| Item Status | Condition | Triggered By |
|---|---|---|
| OPEN | received_qty = 0 |
Initial state |
| PARTIAL | 0 < received_qty < quantity |
GRN creation |
| RECEIVED | received_qty >= quantity - cancelled_qty |
GRN creation |
| CLOSED | Invoiced and payment processed | Invoice completion |
| CANCELLED | Line cancelled by user | Manual action |
SELECT po.po_number, poi.line_number, poi.item_description, poi.quantity - poi.received_qty - poi.cancelled_qty AS outstanding_qty, poi.promised_date, v.vendor_name, CASE WHEN poi.promised_date < CURRENT_DATE THEN 'OVERDUE' ELSE 'ON_TIME' END AS delivery_status FROM po.po_items poi JOIN po.purchase_orders po ON poi.po_id = po.id JOIN vendor.vendors v ON po.vendor_id = v.id WHERE po.status IN ('SENT', 'ACKNOWLEDGED', 'PARTIAL') AND poi.status IN ('OPEN', 'PARTIAL') ORDER BY poi.promised_date;
SELECT v.vendor_name, COUNT(po.id) AS total_pos, SUM(CASE WHEN po.is_fully_received THEN 1 ELSE 0 END) AS fully_received, SUM(CASE WHEN po.is_fully_invoiced THEN 1 ELSE 0 END) AS fully_invoiced, SUM(po.total_amount) AS total_value, SUM(po.total_invoiced_amount) AS invoiced_value FROM po.purchase_orders po JOIN vendor.vendors v ON po.vendor_id = v.id WHERE po.po_date >= :start_date AND po.status NOT IN ('DRAFT', 'CANCELLED') GROUP BY v.id, v.vendor_name ORDER BY total_value DESC;
SELECT po.po_number, poi.line_number, psl.schedule_number, psl.quantity AS scheduled_qty, psl.received_qty, psl.delivery_date, psl.status, l.location_name AS ship_to FROM po.po_schedule_lines psl JOIN po.po_items poi ON psl.po_item_id = poi.id JOIN po.purchase_orders po ON poi.po_id = po.id LEFT JOIN organization.locations l ON psl.ship_to_location_id = l.id WHERE psl.status = 'SCHEDULED' AND psl.delivery_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days' ORDER BY psl.delivery_date;
Always obtain vendor acknowledgment before expecting delivery. Track acknowledged_at timestamp and vendor confirmation reference.
Ensure PO items have proper item_id links for automated matching. Capture unit prices accurately for variance detection.
Document all revision reasons. Avoid frequent revisions by confirming requirements upfront. Use amendments for significant changes.