Back to ER Diagram
Procurement Cycle

Procurement Cycle Logic

Complete procurement lifecycle — MR → PR → RFQ → PO → GRN → Invoice → Payment

PostgreSQL
22 Tables
Schema: procurement
Three-Way Match

Overview

InfraTraq implements a full-cycle procurement workflow covering the complete MR → PR → RFQ → PO → GRN → Invoice → Payment pipeline. The system manages 22 procurement tables, tightly connected to vendor management, receiving, inventory, and finance modules — ensuring end-to-end traceability from material indent to final payment.


Indent (MR)

Requisition (PR)

RFQ

Quotation

Purchase Order

GRN

Invoice

Payment
22
Procurement Tables
8
Lifecycle Stages
3-Way
Match Verification
End-to-End
Audit Trail

Key Principle

  • Every procurement starts with a Material Requisition (MR) raised by the site team against a project/activity
  • Rate-contract items bypass the RFQ stage and generate POs directly
  • Three-way matching (PO ↔ GRN ↔ Invoice) is mandatory before invoice approval
  • All monetary values are tracked in INR (&rupee;) with GST and TDS auto-calculated

Status States

StatusDescriptionAllowed ActionsNext States
DraftMR/PR/PO created but not yet submitted for approvalEdit, Submit, DeletePending Approval
Pending ApprovalAwaiting manager/budget-holder approvalApprove, Reject, ReturnApproved, Rejected
ApprovedMR/PR/PO approved and ready for next stageConvert to PR, Create RFQ, Issue POOpen, In Progress
OpenRFQ open for vendor quotations / PO issued to vendorReceive Quotes, Record GRN, AmendPartial, Closed
PartialPO partially delivered or partially invoicedRecord GRN, Invoice Match, Short-CloseClosed
ClosedFully delivered, invoiced, and paidView, Audit
Short-ClosedPO closed before full delivery; balance releasedView, Audit
CancelledMR/PR/PO cancelled; commitment releasedView, Audit
RejectedApproval denied; returned to originatorEdit, Resubmit, DeleteDraft
HoldInvoice held pending 3-way match resolutionResolve Variance, Re-matchMatched, Cancelled
Matched3-way match passed; invoice auto-approvedSchedule PaymentPaid

Process Flow


Site Team
Raises MR (Indent)

Procurement
Creates PR (Approve)

Decision Point
Rate Contract Exists?

YES → Auto-PO
from Rate Contract
   

NO → Create RFQ
Invite Vendors

Evaluate Quotations
Comparative Statement

Purchase Order (PO) — Approved → Commitment Created — Budget check before approval

GRN (Goods Receipt) — Quality check + Stock update — Accrual entry posted

Three-Way Match — PO qty/rate vs GRN qty vs Invoice qty/rate (±5% tolerance)

MATCH → Auto-Approve
Invoice
   

NO MATCH → Hold
for Resolution

Payment Processing — Schedule per vendor terms — TDS deduction + GST reconcile

Database Schema

procurement.material_requisition

  • mr_id — PK, material indent raised by site team against project activity
  • project_id — FK → project.project
  • requested_by — FK → hr.employee
  • mr_number, mr_date, required_date
  • priority — Urgent / Normal / Low (drives SLA timelines)
  • status — draft, pending_approval, approved, rejected, cancelled

procurement.purchase_requisition

  • pr_id — PK, consolidated requisition grouping one or more MRs for buyer action
  • project_id — FK → project.project
  • mr_id — FK → procurement.material_requisition
  • pr_number, buyer_id — FK → hr.employee
  • status — open, in_progress, closed, cancelled

procurement.rfq

  • rfq_id — PK, request for quotation sent to shortlisted vendors
  • pr_id — FK → procurement.purchase_requisition
  • project_id — FK → project.project
  • rfq_number, closing_date
  • status — open, closed, cancelled

procurement.purchase_order

  • po_id — PK, formal order placed with selected vendor after approval
  • pr_id — FK → procurement.purchase_requisition
  • vendor_id — FK → vendor.vendor
  • project_id — FK → project.project
  • po_number, total_amount
  • status — draft, approved, open, partial, closed, short_closed, cancelled

procurement.po_line

  • id — PK, individual line items within a purchase order
  • po_id — FK → procurement.purchase_order
  • material_id — FK → master_data.material
  • ordered_qty, unit_rate, amount
  • status — open, partial, closed

receiving.grn

  • grn_id — PK, goods received note recorded at store on material delivery
  • po_id — FK → procurement.purchase_order
  • store_id — FK → inventory.store
  • grn_number, total_value
  • status — pending_qc, accepted, rejected, partial

receiving.three_way_match

  • match_id — PK, verification record matching PO, GRN, and vendor invoice
  • po_id — FK → procurement.purchase_order
  • grn_id — FK → receiving.grn
  • invoice_id — FK → accounts_payable.ap_invoice
  • match_status — matched, hold, resolved

procurement.rate_contract

  • rc_id — PK, pre-negotiated rate agreements with vendors for recurring materials
  • vendor_id — FK → vendor.vendor
  • rc_number, valid_from, valid_to
  • status — active, expired, suspended

Step-by-Step Logic

1

Material Requisition (MR)

Site engineer raises an MR in procurement.material_requisition specifying the project, activity, required materials, quantities, and required-by date. The system validates against BOM requirements and checks current stock levels in inventory.stock_ledger before submission. Priority levels (Urgent / Normal / Low) drive SLA timelines.

2

MR Approval & PR Consolidation

Approved MRs are consolidated into a procurement.purchase_requisition (PR). Multiple MRs for the same material category or vendor can be grouped into a single PR to leverage bulk pricing. The PR is assigned to a buyer (buyer_id) who manages the sourcing process.

3

RFQ Creation & Vendor Invitation

For non-rate-contract items, the buyer creates an procurement.rfq and invites shortlisted vendors. The RFQ includes material specifications, quantities, delivery schedule, and closing date. Minimum 3 vendors must be invited per company policy. Vendor selection draws from the approved vendor list.

4

Quotation Evaluation & Comparative Statement

Received quotations are evaluated using a weighted scoring model (price 60%, delivery 20%, quality 10%, terms 10%). The system generates an auto-ranked comparative statement. Technical and commercial evaluations can be split for high-value procurements. L1 (lowest) vendor is recommended unless overridden with justification.

5

Purchase Order (PO) Creation

PO is created in procurement.purchase_order with line items in procurement.po_line. For rate-contract items, the PO is auto-generated from procurement.rate_contract without the RFQ stage. Budget availability is checked before approval. On approval, a financial commitment is created in finance.commitment.

6

GRN Recording at Store

When material arrives, the store team records a GRN in receiving.grn. Quantities are verified against PO line items (tolerance ±5%). Quality inspection is performed per QMS standards. Accepted quantities update inventory.stock_ledger and trigger an accrual journal entry. Rejected quantities initiate a vendor return process.

7

Three-Way Matching (PO ↔ GRN ↔ Invoice)

The receiving.three_way_match table records the verification of PO quantities/rates against GRN received quantities against invoice billed quantities/rates. Tolerance is ±5% on quantity and ±2% on price. Matched records auto-approve the invoice; mismatches are held for manual resolution.

8

Invoice Booking & Payment Processing

Matched invoices are booked in accounts_payable.ap_invoice. Accrual provisional entries are reversed and final journal entries posted. GST input credit is captured, TDS is auto-deducted per vendor category. Payments are scheduled per vendor payment terms (Net 30/45/60) and processed via accounts_payable.payment_voucher.

Code Implementation

class ProcurementService {

  /** Step 1: Create Material Requisition — validates stock & BOM */
  async createMR(projectId, items) {
    for (const item of items) {
      const stock = await StockLedger.getBalance(item.material_id, projectId);
      const bomReq = await BOM.getRequirement(projectId, item.material_id);
      if (stock.available >= item.qty) throw new Error('Sufficient stock exists — issue from store');
      item.net_qty = item.qty - stock.available;
    }
    const mr = await MaterialRequisition.create({
      project_id: projectId, mr_number: await Sequence.next('MR'),
      mr_date: new Date(), status: 'draft', items
    });
    return mr;
  }

  /** Step 2: Convert approved MR(s) into Purchase Requisition */
  async convertMRtoPR(mrId) {
    const mr = await MaterialRequisition.findById(mrId);
    if (mr.status !== 'approved') throw new Error('MR must be approved');
    // Group MR lines by vendor preference / material category
    const groups = groupBy(mr.items, 'material_category');
    const prs = [];
    for (const [category, lines] of Object.entries(groups)) {
      const pr = await PurchaseRequisition.create({
        project_id: mr.project_id, mr_id: mrId,
        pr_number: await Sequence.next('PR'),
        buyer_id: await BuyerAssignment.resolve(category),
        status: 'open', items: lines
      });
      prs.push(pr);
    }
    return prs;
  }

  /** Step 3: Create RFQ and send to shortlisted vendors */
  async createRFQ(prId, vendorIds) {
    if (vendorIds.length < 3) throw new Error('Minimum 3 vendors required for RFQ');
    const pr = await PurchaseRequisition.findById(prId);
    const rfq = await RFQ.create({
      pr_id: prId, project_id: pr.project_id,
      rfq_number: await Sequence.next('RFQ'),
      closing_date: addDays(new Date(), 7),
      status: 'open', vendor_ids: vendorIds
    });
    await NotificationService.sendRFQInvitations(rfq, vendorIds);
    return rfq;
  }

  /** Step 4: Evaluate quotations and generate comparative statement */
  async evaluateQuotations(rfqId) {
    const quotes = await Quotation.findByRFQ(rfqId);
    const scored = quotes.map(q => ({
      vendor_id: q.vendor_id,
      price_score:  normalize(q.total, quotes, 'total')  * 0.6,
      delivery_score: normalize(q.lead_days, quotes, 'lead_days') * 0.2,
      quality_score:  q.vendor_rating * 0.1,
      terms_score:   normalize(q.credit_days, quotes, 'credit_days') * 0.1,
    }));
    scored.forEach(s => s.total_score = s.price_score + s.delivery_score
                                        + s.quality_score + s.terms_score);
    scored.sort((a, b) => b.total_score - a.total_score);
    return { comparative_statement: scored, recommended: scored[0] };
  }

  /** Step 5: Create Purchase Order — checks budget, creates commitment */
  async createPO(prId, vendorId) {
    const pr = await PurchaseRequisition.findById(prId);
    const poTotal = pr.items.reduce((s, i) => s + i.qty * i.unit_rate, 0);
    // Budget availability check
    await CommitmentService.checkBudgetAvailability(
      pr.project_id, pr.cost_code_id, poTotal
    );
    // Check for duplicate active PO for same PR
    const existing = await PurchaseOrder.findOne({
      pr_id: prId, vendor_id: vendorId, status: ['draft', 'approved']
    });
    if (existing) throw new Error('Active PO already exists for this PR + vendor');

    const po = await PurchaseOrder.create({
      pr_id: prId, vendor_id: vendorId, project_id: pr.project_id,
      po_number: await Sequence.next('PO'),
      total_amount: poTotal, status: 'draft'
    });
    // On approval → create financial commitment
    await CommitmentService.createCommitment('PO', po.po_id);
    return po;
  }

  /** Step 6: Process GRN — quality check, stock update */
  async processGRN(poId, receivedItems) {
    const po = await PurchaseOrder.findById(poId);
    for (const item of receivedItems) {
      const poLine = await POLine.findOne({ po_id: poId, material_id: item.material_id });
      const variance = Math.abs(item.received_qty - poLine.ordered_qty) / poLine.ordered_qty;
      if (variance > 0.05) throw new Error('GRN qty exceeds ±5% tolerance');
      // Quality inspection
      item.qc_status = await QualityService.inspect(item);
    }
    const grn = await GRN.create({
      po_id: poId, store_id: po.delivery_store_id,
      grn_number: await Sequence.next('GRN'),
      total_value: receivedItems.reduce((s, i) => s + i.received_qty * i.unit_rate, 0),
      status: 'accepted', items: receivedItems.filter(i => i.qc_status === 'pass')
    });
    // Update inventory stock ledger
    await InventoryService.updateStock(grn);
    // Post accrual entry
    await CommitmentService.createAccrual(po.commitment_id, grn.grn_id, grn.total_value);
    return grn;
  }

  /** Step 7: Three-way match — validates qty and price within tolerance */
  async threeWayMatch(poId, grnId, invoiceId) {
    const po = await PurchaseOrder.findById(poId);
    const grn = await GRN.findById(grnId);
    const inv = await APInvoice.findById(invoiceId);

    const qtyMatch  = Math.abs(grn.total_qty - inv.total_qty) / grn.total_qty <= 0.05;
    const rateMatch = Math.abs(po.unit_rate - inv.unit_rate) / po.unit_rate <= 0.02;
    const match_status = (qtyMatch && rateMatch) ? 'matched' : 'hold';

    const match = await ThreeWayMatch.create({
      po_id: poId, grn_id: grnId, invoice_id: invoiceId, match_status
    });
    if (match_status === 'matched') {
      await APInvoice.approve(invoiceId);
      await CommitmentService.onInvoiceBooked(po.commitment_id, inv.total_amount);
    }
    return match;
  }
}

Validation Rules

RuleConditionAction
Budget Availability CheckPO total > available budget for cost codeBlock PO creation, notify finance manager
Minimum Quotes RequiredRFQ has fewer than 3 vendor quotationsPrevent PO creation, extend RFQ deadline
Rate Contract ValidityRate contract expired or quantity ceiling exceededBlock auto-PO, route to manual RFQ process
GRN Quantity ToleranceReceived qty deviates > ±5% from PO line qtyHold GRN for store manager approval
Price Variance ThresholdInvoice unit rate deviates > ±2% from PO rateHold invoice, escalate to procurement head
Duplicate PO PreventionActive PO already exists for same PR + vendorBlock creation, show existing PO reference
MR Stock CheckSufficient stock exists in project storeReject MR, suggest store issue instead
Vendor Blacklist CheckSelected vendor is on blacklist or suspendedBlock PO/RFQ, notify compliance team

Automated Actions & Triggers

EventSource TableAuto Action
MR Approvedprocurement.material_requisitionAuto-create Purchase Requisition (PR) grouped by category
PO Approvedprocurement.purchase_orderCreate financial commitment in finance.commitment
GRN Acceptedreceiving.grnUpdate inventory.stock_ledger + post accrual journal entry
3-Way Match Passreceiving.three_way_matchAuto-approve AP invoice for payment scheduling
Invoice Approvedaccounts_payable.ap_invoiceSchedule payment per vendor payment terms (Net 30/45/60)
Rate Contract Expiry (T-30)procurement.rate_contractAlert procurement team to initiate renewal RFQ
Reorder Point Hitinventory.stock_ledgerAuto-generate MR for materials below reorder level
PO Short-Closeprocurement.purchase_orderRelease uncommitted balance back to available budget

Integration Points

Upstream (Data Sources)

  • Estimation BOQ/BOM — Material quantities and specifications feed MR validation
  • Inventory Reorder Alerts — Stock falling below reorder point auto-triggers MR
  • Project Activity Requirements — Activity schedules drive material required-by dates
  • Vendor Master — Approved vendor list, ratings, and blacklist status
  • Budget Allocation — Cost code budgets gate PO approval

Downstream (Consumers)

  • Inventory — GRN updates stock ledger, bin locations, and valuation
  • Finance (Commitment/AP) — PO creates commitment; invoice creates AP liability
  • Subcontractor Material Supply — Issue-to-subcontractor from procured stock
  • Analytics & Spend Reports — Category-wise, vendor-wise, project-wise spend analysis
  • GST/TDS Compliance — Tax entries auto-generated for filing and reconciliation

Best Practices

Implementation Guidelines

  • Rate Contract Usage — Establish rate contracts for high-volume recurring materials (cement, steel, aggregates) to skip RFQ and expedite procurement
  • Vendor Rotation — Distribute orders across approved vendors to avoid single-source dependency; track vendor concentration ratio
  • Lead Time Tracking — Record actual vs. promised delivery dates to build vendor reliability scores for future evaluations
  • Emergency Procurement — Define a fast-track approval path (single-quote allowed) for urgent items with post-facto documentation; cap at &rupee;50,000 per instance
  • Consolidation Windows — Batch MRs weekly to maximize consolidation and bulk discount opportunities
  • GRN Timeliness — Record GRN within 24 hours of material receipt to ensure accurate stock and accrual records
  • Short-Close Policy — Auto-close POs with no activity for 90 days to release blocked budget

Common Pitfalls

  • Raising POs without MR/PR trail — breaks audit traceability and compliance requirements
  • Ignoring rate contract expiry — leads to orders at old rates causing vendor disputes
  • Delayed GRN entry — stock records become unreliable, duplicate MRs are raised for materials already on-site
  • Skipping 3-way match for urgency — results in overpayment and audit findings
  • Single-vendor dependency — supply chain risk; enforce minimum 2 approved vendors per material category
  • Not tracking partial deliveries — PO shows open but most material already received, blocking budget unnecessarily