Back to ER Diagram
Commitment Accounting

Commitment Accounting Logic

5-Layer commitment model that tracks every rupee from budget allocation through final payment — ensuring real-time visibility into financial obligations at any project phase.

PostgreSQL
18 Tables
Schema: finance
Real-time Balance Tracking

Overview

InfraTraq implements a 5-Layer Commitment Accounting model that tracks every rupee from budget allocation through final payment. This ensures real-time visibility into financial obligations at any project phase, preventing budget overruns and enabling proactive cash-flow management.


Budget

Commitment

Accrual

Invoice

Payment
Layer 1 — BUDGET Annual/project budget allocation per cost code
Layer 2 — COMMITMENT PO/WO value committed to vendors/subcontractors
Layer 3 — ACCRUAL GRN/DMB received but not yet invoiced
Layer 4 — INVOICE Vendor/subcontractor invoice booked
Layer 5 — PAYMENT Actual cash outflow via payment voucher
5
Commitment Layers
14
Finance Tables
549
FK Relationships
Real-time
Balance Tracking

Status States

StatusDescriptionAllowed ActionsNext States
DraftBudget/commitment created but not yet approvedEdit, Submit for Approval, DeletePending Approval
Pending ApprovalAwaiting budget holder or finance manager approvalApprove, Reject, ReturnApproved, Rejected
ApprovedBudget allocated or commitment confirmedAmend, Consume (via PO/GRN)Open, Amended
OpenActive commitment with remaining balanceInvoice Match, Part-Pay, AmendPartial, Closed
PartialCommitment partially invoiced/paidInvoice Match, Pay, Short-CloseClosed
ClosedFully delivered and paid; no remaining balanceView, Audit
CancelledBudget or commitment cancelled; balance releasedView, Audit
RejectedApproval denied; returned to originatorEdit, Resubmit, DeleteDraft

The Five Commitment Layers

Layer 1 — Budget

Annual/project budget allocation per cost code. Finance team creates budget lines with monthly phasing (m1-m12) for cash-flow forecasting. Approved budget sets the spending ceiling.

Layer 2 — Commitment

PO/WO value committed to vendors/subcontractors. Created automatically when PO or WO is approved. Budget availability check runs before approval.

Layer 3 — Accrual

GRN/DMB received but not yet invoiced. Creates provisional journal entries debiting expense and crediting accrual liability.

Layers 4-5 — Invoice & Payment

Invoice booking after 3-way match reverses accruals and posts final JE. Payment updates commitment and posts bank entries. TDS tracked separately.

Key Principle

  • Available Budget = Budget − Commitment − Accrual (uninvoiced) − Invoice (unpaid)
  • Each layer reduces the remaining budget progressively
  • At any point: Budget ≥ Commitment ≥ Accrual ≥ Invoice ≥ Payment
  • Variance at Completion (VAC) = BAC − EAC

Database Schema

finance.budget

  • budget_id — PK, budget header per project/entity/period
  • project_id — FK → project.project
  • entity_id — FK → organization.company_entity
  • budget_number, budget_type, fiscal_year
  • total_amount, currency, status

finance.budget_line

  • id — PK, budget line items per account/cost code
  • budget_id — FK → finance.budget
  • account_id — FK → finance.gl_account
  • cost_code_id — FK → master_data.cost_code
  • annual_amount, m1..m12 — Monthly phasing
  • revised_amount, actual_amount, variance

finance.commitment

  • commitment_id — PK, financial commitment tracking
  • project_id — FK → project.project
  • po_id — FK → procurement.purchase_order
  • wo_id — FK → subcontractor.work_order
  • commitment_type — PO or WO
  • original_amount, revised_amount, invoiced_amount, paid_amount, balance
  • status — open, partial, closed, cancelled

finance.commitment_line

  • id — PK, commitment line items per GL account
  • commitment_id — FK → finance.commitment
  • account_id — FK → finance.gl_account
  • description, amount, invoiced, paid, balance

finance.provisional_entry

  • id — PK, month-end provisional (accrual) journal entries
  • entity_id — FK → organization.company_entity
  • entry_type — accrual, reversal
  • reference_type, reference_id — Source document
  • amount, je_id — FK → finance.journal_entry
  • is_reversed, status

finance.journal_entry / journal_line

  • je_id — PK, GL journal entries for all commitment layer postings
  • je_type — commitment, accrual, reversal, invoice, payment
  • source_module, reference_type, reference_id
  • total_debit, total_credit, status
  • account_id — FK → finance.gl_account (on journal_line)
  • cost_code_id — FK → master_data.cost_code (on journal_line)

Step-by-Step Logic

1

Budget Allocation

Finance team creates budget + budget_line records per project, cost code, and GL account. Monthly phasing (m1-m12) enables cash-flow forecasting. Approved budget sets the ceiling.

2

Commitment Creation (PO/WO Approval)

When a Purchase Order or Work Order is approved, the system auto-creates a commitment record with original_amount = PO/WO total. Each line maps to a GL account via commitment_line. Budget availability check runs before approval — if insufficient budget, the PO/WO is blocked.

3

Accrual on Receipt (GRN/DMB)

When goods are received (GRN) or work is measured (DMB), the system creates a provisional_entry debiting expense and crediting accrual liability. This captures the obligation before the invoice arrives.

4

Invoice Booking (3-Way Match)

When the vendor's AP Invoice is booked after 3-way match (PO-GRN-Invoice), the provisional entry is reversed and a final journal_entry is posted. The commitment record's invoiced_amount is updated. Tax entries (GST/TDS) are auto-generated.

5

Payment Release

Payment Voucher creation updates commitment.paid_amount and posts bank/cash journal entries. TDS deducted is tracked separately. The commitment balance = revised_amount - paid_amount shows remaining obligation.

6

Commitment Closure

When PO/WO is fully delivered and paid, or cancelled with partial delivery, the commitment is closed. Any remaining balance is released back to the available budget. Short-close entries reverse the uncommitted portion.

Code Implementation

class CommitmentService {

  /** Check if budget has sufficient balance for a new commitment */
  async checkBudgetAvailability(projectId, costCodeId, amount) {
    const budget = await BudgetLine.findOne({
      project_id: projectId, cost_code_id: costCodeId, status: 'approved'
    });
    const committed = await Commitment.sum('revised_amount', {
      project_id: projectId, cost_code_id: costCodeId, status: ['open', 'partial']
    });
    const available = budget.revised_amount - committed;
    if (amount > available) throw new InsufficientBudgetError(available, amount);
    return { available, requested: amount, remaining: available - amount };
  }

  /** Create commitment when PO/WO is approved */
  async createCommitment(referenceType, referenceId) {
    const doc = await (referenceType === 'PO'
      ? PurchaseOrder.findById(referenceId)
      : WorkOrder.findById(referenceId));
    await this.checkBudgetAvailability(doc.project_id, doc.cost_code_id, doc.total_amount);
    const commitment = await Commitment.create({
      project_id: doc.project_id,
      commitment_type: referenceType,
      po_id: referenceType === 'PO' ? referenceId : null,
      wo_id: referenceType === 'WO' ? referenceId : null,
      original_amount: doc.total_amount, revised_amount: doc.total_amount,
      invoiced_amount: 0, paid_amount: 0,
      balance: doc.total_amount, status: 'open'
    });
    // Post commitment journal: Dr Commitment Control, Cr Commitment Liability
    await JournalService.postCommitment(commitment);
    return commitment;
  }

  /** Create accrual when GRN/DMB is approved */
  async createAccrual(commitmentId, grnOrDmbId, amount) {
    const entry = await ProvisionalEntry.create({
      entity_id, entry_type: 'accrual',
      reference_type: 'GRN', reference_id: grnOrDmbId,
      amount, status: 'active'
    });
    // Post provisional JE: Dr Expense, Cr Accrual Liability
    await JournalService.postAccrual(entry);
    return entry;
  }

  /** Update commitment when invoice is booked */
  async onInvoiceBooked(commitmentId, invoiceAmount) {
    await Commitment.update(commitmentId, {
      invoiced_amount: literal('invoiced_amount + ' + invoiceAmount),
      balance: literal('revised_amount - paid_amount')
    });
    await ProvisionalEntry.reverse(commitmentId);
  }

  /** Update commitment when payment is released */
  async onPaymentReleased(commitmentId, paidAmount) {
    await Commitment.update(commitmentId, {
      paid_amount: literal('paid_amount + ' + paidAmount),
      balance: literal('revised_amount - paid_amount')
    });
    const c = await Commitment.findById(commitmentId);
    if (c.balance <= 0) await c.update({ status: 'closed' });
  }
}

Validation Rules

RuleConditionAction
Budget Ceiling CheckNew PO/WO amount > available budgetBlock approval, notify finance manager
Over-commitment AlertCommitted > 90% of budgetWarning notification to PM and Finance
Tolerance VarianceInvoice amount > PO amount + tolerance%Require additional approval level
Period LockTransaction date in locked periodReject entry, suggest next open period
Duplicate CommitmentSame PO/WO already has active commitmentBlock creation, show existing record
Amendment RecheckPO/WO amendment changes valueRerun budget check for delta amount

Automated Actions & Triggers

EventSource TableAuto Action
PO Approvedprocurement.purchase_orderCreate commitment + post JE
WO Approvedsubcontractor.work_orderCreate commitment + post JE
GRN Acceptedreceiving.grnCreate accrual provisional entry + JE
DMB Approvedsubcontractor.dmbCreate accrual provisional entry + JE
Invoice 3-Way Matchedaccounts_payable.ap_invoiceReverse accrual + final JE + update commitment
Payment Releasedaccounts_payable.payment_voucherUpdate commitment.paid_amount + bank JE
PO/WO Amendedprocurement.po_amendmentUpdate commitment.revised_amount + recheck budget
PO/WO Cancelledprocurement.purchase_orderClose commitment + release budget
Period Closefinance.fiscal_periodGenerate all pending accruals for the period

Integration Points

Upstream (Data Sources)

  • Procurement — PO creation triggers commitment
  • Subcontractor — WO creation triggers commitment
  • Receiving — GRN triggers accrual layer
  • Subcontractor — DMB triggers accrual layer
  • Estimation — BOQ feeds budget allocation

Downstream (Consumers)

  • Accounts Payable — Invoice booking updates commitment
  • Treasury — Cash flow forecast uses commitment pipeline
  • EVM — ACWP calculated from commitment data
  • Analytics — Budget utilization dashboards
  • Approval Workflow — Budget check gates PO/WO approval

Best Practices

Implementation Guidelines

  • Always run budget availability check inside a database transaction to avoid race conditions
  • Use database-level constraints: CHECK (paid_amount ≤ invoiced_amount ≤ revised_amount)
  • Index commitment on (project_id, cost_code_id, status) for fast availability queries
  • Month-end accrual generation should be idempotent — re-running produces same result
  • Short-close POs within 30 days of last GRN to release blocked budget
  • Maintain audit trail via audit.audit_trail for all commitment state changes

Common Pitfalls

  • Forgetting to update commitment on PO amendment — leads to phantom commitments
  • Not reversing accruals when invoice arrives — double-counting expenses
  • Allowing backdated entries in locked periods — breaks period-end reports
  • Rate contract POs without budgeted ceiling — unlimited commitment exposure