Earned Value Management — PV, EV, AC, CPI, SPI tracking for project cost and schedule control
InfraTraq implements a full Earned Value Management System (EVMS) with Performance Measurement Baseline (PMB), Control Accounts, Work Packages, S-curves, and comprehensive forecasting (EAC/ETC/IEAC). The system provides objective, quantitative project performance measurement by integrating scope, schedule, and cost into a single framework for proactive decision-making.
Core EVM metrics with traffic-light threshold indicators for at-a-glance project health assessment.
| Status | Description | Allowed Actions | Next States |
|---|---|---|---|
| Draft | Baseline or control account created but not approved | Edit, Submit for Approval, Delete | Pending Approval |
| Pending Approval | Awaiting PMB approval by project sponsor | Approve, Reject, Return | Approved, Rejected |
| Approved | Baseline is current; control accounts active | Collect Data, Amend Baseline | Active, Re-baseline |
| Active | EVM data collection and analysis in progress | Collect Period Data, Forecast, Report | Complete, Re-baseline |
| Watch | SPI/CPI between warning and critical thresholds | Review, Create Action Plan | Active, Escalated |
| Escalated | SPI/CPI below critical threshold; mgmt intervention | Corrective Action, Re-baseline, Audit | Active, Re-baseline |
| Re-baseline | Formal baseline change in progress | Review Change, Approve New PMB | Approved |
| Complete | Project EVM tracking closed; final metrics archived | View, Audit, Lessons Learned | — |
baseline_id — PK, Performance Measurement Baseline headerproject_id — FK → project.projecttotal_bac — Budget at Completion for the projecttime_phased_budget — JSON: period-by-period planned value distributionis_current — Boolean, only one active baseline per projectca_id — PK, control account linking WBS to EVM metricsproject_id — FK → project.projectwbs_id — FK → project.wbs_elementbac — Budget at Completion for this control accounteac, etc — Estimate at/to Completionacwp, bcwp, bcws — Actual Cost, Earned Value, Planned Valuespi, cpi — Schedule and Cost Performance Indiceswp_id — PK, work package within a control accountca_id — FK → evms.control_accountactivity_id — FK → project.activitybudget, actual_cost, earned_value — WP-level trackingevm_method — 0/100, 50/50, % Complete, Milestones, or LOEid — PK, period-level EVM data recordproject_id — FK → project.projectca_id — FK → evms.control_accountperiod_date — Reporting period datebcws, bcwp, acwp — Three data streamssv, cv — Schedule and Cost Variancespi, cpi — Performance indiceseac — Estimate at Completion for the periodid — PK, project-level EAC/ETC forecast recordproject_id — FK → project.projectforecast_type — CPI_BASED, SPI_CPI, BOTTOM_UPeac, etc — Estimated amountsestimated_completion_date — Projected finishid — PK, cumulative S-curve data pointproject_id — FK → project.projectdata_date — Period date for the data pointplanned_cumulative — Cumulative BCWS (PV)actual_cumulative — Cumulative ACWP (AC)earned_cumulative — Cumulative BCWP (EV)id — PK, configurable threshold recordproject_id — FK → project.projectmetric — spi, cpi, sv, cv, etc.warning_threshold — Yellow alert trigger valuecritical_threshold — Red escalation trigger valueThe Performance Measurement Baseline (PMB) is established by loading the approved Budget at Completion (BAC) into evms.pm_baseline with time-phased distribution across project periods. The baseline represents the cumulative planned value (BCWS) curve against which performance is measured. Only one baseline can be marked is_current = true per project.
Each work package in evms.work_package is assigned an earned value measurement method: 0/100 (credit at completion), 50/50 (50% at start, 50% at finish), % Complete (weighted by physical progress), Milestones (weighted milestone values), or Level of Effort (time-apportioned). Method selection depends on work package duration, measurability, and type.
Each reporting period (typically monthly), three data streams are collected: BCWS from the time-phased baseline, BCWP calculated from physical % complete applied to each work package budget using the assigned EVM method, and ACWP from the finance module's actual cost postings. Data is stored in evms.earned_value.
Schedule Variance (SV = BCWP − BCWS) and Cost Variance (CV = BCWP − ACWP) are computed at control account and project levels. Positive values indicate favourable performance. The SPI and CPI indices are checked against evms.evm_threshold to trigger traffic-light status and alerts.
Multiple Estimate at Completion (EAC) methods are calculated and stored in evms.evm_forecast: EAC = BAC / CPI (assumes current cost efficiency continues), EAC = AC + ETC (manager's bottom-up re-estimate), and EAC = AC + (BAC − EV) / CPI (remaining work at current efficiency). The ETC (Estimate to Complete) = EAC − ACWP. VAC = BAC − EAC shows projected overrun/underrun.
Cumulative planned, earned, and actual values are plotted as S-curves in evms.s_curve_data. Each period close appends a data point with planned_cumulative, earned_cumulative, and actual_cumulative values. The three curves visually show schedule and cost deviations — earned below planned indicates schedule delay; actual above earned indicates cost overrun.
EVM reports are generated at control account, WBS, and project levels, including variance analysis summaries, trend charts, TCPI (To-Complete Performance Index), and corrective action recommendations. Reports feed into the analytics dashboards and risk assessment modules for holistic project oversight.
class EVMService { /** Establish the Performance Measurement Baseline for a project */ async establishBaseline(projectId) { const project = await Project.findById(projectId); const wbsItems = await WBS.findAll({ project_id: projectId }); const totalBAC = wbsItems.reduce((sum, w) => sum + w.budget, 0); // Deactivate any existing current baseline await PMBaseline.update({ is_current: false }, { where: { project_id: projectId, is_current: true } }); const baseline = await PMBaseline.create({ project_id: projectId, total_bac: totalBAC, time_phased_budget: this.distributeByPeriod(wbsItems, project), is_current: true }); // Create control accounts for each WBS element for (const wbs of wbsItems) { await ControlAccount.create({ project_id: projectId, wbs_id: wbs.id, bac: wbs.budget, eac: wbs.budget, etc: wbs.budget, acwp: 0, bcwp: 0, bcws: 0, spi: 1.0, cpi: 1.0 }); } return baseline; } /** Collect period data — BCWS, BCWP, ACWP for all control accounts */ async collectPeriodData(projectId, periodDate) { const baseline = await PMBaseline.findOne({ project_id: projectId, is_current: true }); const accounts = await ControlAccount.findAll({ project_id: projectId }); for (const ca of accounts) { const bcws = this.getBCWSFromBaseline(baseline, ca.wbs_id, periodDate); const bcwp = await this.calculateBCWP(ca.ca_id, periodDate); const acwp = await FinanceService.getActualCost(ca.wbs_id, periodDate); await this.calculateEVM(ca.ca_id, periodDate, bcws, bcwp, acwp); } await this.generateSCurve(projectId, periodDate); await this.checkThresholds(projectId); } /** Calculate all EVM metrics for a control account */ async calculateEVM(caId, periodDate, bcws, bcwp, acwp) { const ca = await ControlAccount.findById(caId); const sv = bcwp - bcws; // Schedule Variance const cv = bcwp - acwp; // Cost Variance const spi = bcws > 0 ? bcwp / bcws : 1.0; // Schedule Perf Index const cpi = acwp > 0 ? bcwp / acwp : 1.0; // Cost Perf Index const eac = cpi > 0 ? ca.bac / cpi : ca.bac; // Estimate at Completion const etc = eac - acwp; // Estimate to Complete const vac = ca.bac - eac; // Variance at Completion const tcpi = (ca.bac - bcwp) > 0 // To-Complete Perf Index ? (ca.bac - bcwp) / (ca.bac - acwp) : 1.0; // Persist period EVM data await EarnedValue.create({ project_id: ca.project_id, ca_id: caId, period_date: periodDate, bcws, bcwp, acwp, sv, cv, spi, cpi, eac }); // Update control account running totals await ControlAccount.update(caId, { bcws, bcwp, acwp, spi, cpi, eac, etc }); return { sv, cv, spi, cpi, eac, etc, vac, tcpi }; } /** Generate S-curve data point for the period */ async generateSCurve(projectId, periodDate) { const accounts = await ControlAccount.findAll({ project_id: projectId }); const planned = accounts.reduce((s, ca) => s + ca.bcws, 0); const earned = accounts.reduce((s, ca) => s + ca.bcwp, 0); const actual = accounts.reduce((s, ca) => s + ca.acwp, 0); await SCurveData.create({ project_id: projectId, data_date: periodDate, planned_cumulative: planned, earned_cumulative: earned, actual_cumulative: actual }); } /** Check EVM thresholds and raise alerts */ async checkThresholds(projectId) { const thresholds = await EVMThreshold.findAll({ project_id: projectId }); const latest = await EarnedValue.findLatest(projectId); for (const t of thresholds) { const value = latest[t.metric]; // e.g., 'spi', 'cpi' if (value < t.critical_threshold) { await AlertService.escalate(projectId, t.metric, value, 'critical'); } else if (value < t.warning_threshold) { await AlertService.notify(projectId, t.metric, value, 'warning'); } } } /** Forecast completion using multiple EAC methods */ async forecastCompletion(projectId) { const ev = await EarnedValue.findLatest(projectId); const bac = await PMBaseline.getBAC(projectId); const forecasts = [ { type: 'CPI_BASED', eac: bac / ev.cpi, etc: (bac / ev.cpi) - ev.acwp }, { type: 'SPI_CPI', eac: ev.acwp + (bac - ev.bcwp) / (ev.cpi * ev.spi), etc: (bac - ev.bcwp) / (ev.cpi * ev.spi) }, { type: 'BOTTOM_UP', eac: ev.acwp + await this.getBottomUpETC(projectId), etc: await this.getBottomUpETC(projectId) }, ]; for (const f of forecasts) { await EVMForecast.upsert({ project_id: projectId, forecast_type: f.type, eac: f.eac, etc: f.etc, estimated_completion_date: this.estimateDate(f.eac, ev) }); } return forecasts; } }
| Rule | Condition | Action |
|---|---|---|
| BAC Integrity Check | BAC ≠ sum of control account budgets | Block baseline approval, show discrepancy |
| EVM Method Required | Work package has no evm_method assigned | Block period data collection, flag WP for setup |
| ACWP Reconciliation | ACWP does not match finance actuals for period | Halt EVM calculation, notify finance team |
| BCWP Ceiling Check | Cumulative BCWP exceeds BAC for a control account | Cap BCWP at BAC, generate warning |
| S-Curve Monotonicity | Cumulative S-curve data point less than previous | Reject data point, flag data quality issue |
| Division by Zero Guard | BCWS = 0 or ACWP = 0 when computing SPI/CPI | Default index to 1.0, flag as insufficient data |
| Event | Source Table | Auto Action |
|---|---|---|
| Baseline Approved | evms.pm_baseline | Snapshot time-phased budget, create control accounts, populate S-curve plan line |
| Period Data Collected | evms.earned_value | Auto-calculate all EVM metrics (SPI, CPI, SV, CV, EAC, ETC, VAC, TCPI) |
| SPI or CPI < Warning Threshold | evms.evm_threshold | Alert PM with variance analysis summary |
| SPI or CPI < Critical Threshold | evms.evm_threshold | Escalate to senior management, require corrective action plan |
| EAC > BAC + Contingency | evms.evm_forecast | Escalation to project sponsor, trigger budget revision workflow |
| Monthly Period Close | finance.fiscal_period | Generate S-curve data point, update all forecasts, produce EVM report |
| Baseline Change Approved | evms.pm_baseline | Re-snapshot PMB, recalculate cumulative BCWS, log baseline revision |
evms.earned_value on (project_id, period_date) and (ca_id, period_date) for fast queries