The average ROI for ERP implementations is 52% — meaning every dollar invested returns $1.52 — with most companies recovering their investment within 16 months. For professional services and construction businesses in Indonesia, project costing is often the module with the fastest ROI: knowing exactly how much each project costs versus what it billed can turn unprofitable project types into profitable ones within a single quarter. At Commsult, I built a project costing module for a consulting firm client that tracks time, expenses, and cost allocations against project budgets in real time. Here's the architecture.
A project has one or more budget lines, each tied to a cost category: Labor, Travel, Materials, Subcontractor, Other. Each budget line has a budgeted amount and optionally a budgeted quantity (hours, trips, units). The project module tracks actuals against each budget line as costs are posted. Actuals flow in from three sources: timesheets (labor costs), expense claims (travel, other), and AP invoices tagged to the project (materials, subcontractors). The budget vs. actual comparison is the core output.
Staff submit weekly timesheets via a simple React UI: select project, select task (a sub-unit of the project), enter hours per day, add a description. The timesheet goes to the project manager for approval. Once approved, the hours are multiplied by the staff member's cost rate (stored in HR module) to compute the labor cost, which posts automatically to the project's journal as a debit to the project cost account. This fully automated costing eliminates the error-prone manual calculation of 'how many hours did we spend and what did it cost us' that plagues professional services firms.
Project Costing Data Flow
┌─────────────────────────────────────────────────────┐
│ COST SOURCES │
│ │
│ Timesheets Expense Claims AP Invoices │
│ (Labor) (Travel/Other) (Materials) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ hours × cost_rate receipts vendor bills │
└──────────────────────────┬──────────────────────────┘
│ all auto-post to
▼
┌─────────────────────────────────────────────────────┐
│ journal_lines (project_id tagged) │
│ DEBIT Project WIP [cost amount] │
│ CREDIT Accrued Labor [or Expense Payable] │
└──────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Budget vs. Actual Dashboard │
│ │
│ Category Budget Actual Variance Status │
│ Labor 200M 165M +35M 🟢 83% │
│ Travel 15M 18M -3M 🔴 120% │
│ Materials 300M 280M +20M 🟡 93% │
│ Subcontract 50M 40M +10M 🟢 80% │
│ ───────────────────────────────────────────────── │
│ TOTAL 565M 503M +62M 89% │
└─────────────────────────────────────────────────────┘From my experience building ERP systems at Commsult: always capture the billable/non-billable distinction on each timesheet entry. Time spent on internal meetings, proposal writing, and rework is non-billable even if it's on a client project. The billable vs. total hours ratio by project manager is one of the most revealing metrics — it shows who is efficient and who is spending excessive time on overhead. Clients are consistently surprised by their actual billability rates when they first see this data.
Project expenses — travel, accommodation, client entertainment — are submitted via expense claims linked to a project. The expense claim workflow mirrors a mini AP process: staff submit receipts (photo upload), project manager approves, finance verifies and reimburses. Approved expense claims post to the project's cost ledger automatically. The project dashboard shows both labor costs (from timesheets) and direct costs (from expenses and AP invoices) in real time.
Our system supports three billing models: Time and Materials (bill actual hours × agreed rate + reimbursable expenses), Fixed Price (milestone-based billing regardless of cost), and Cost Plus (cost + agreed margin%). For T&M projects, the system generates draft AR invoices from approved timesheets and expenses. For Fixed Price, the AR invoice is triggered by project milestones. For Cost Plus, the invoice is the cost total × (1 + margin%). The billing model is set at project creation and determines which invoice generation logic runs.
-- PostgreSQL: project cost tables
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
billing_model VARCHAR(20) NOT NULL -- 'TM', 'FIXED', 'COST_PLUS'
);
CREATE TABLE project_budget_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id),
category VARCHAR(50) NOT NULL, -- 'LABOR','TRAVEL','MATERIALS'
budgeted_amt NUMERIC(15,2) NOT NULL,
budgeted_qty NUMERIC(10,2)
);
-- NestJS: approve timesheet and post cost to ledger
@Transactional()
async approveTimesheet(timesheetId: string) {
const ts = await this.tsRepo.findOneOrFail(timesheetId, {
relations: ['employee', 'project'],
});
const costRate = await this.hrService.getCostRate(ts.employee.id);
const laborCost = ts.hours * costRate;
// Post to journal
await this.journalService.post({
description: `Labor: ${ts.employee.name} - ${ts.project.code}`,
lines: [
{ accountId: ACCOUNTS.PROJECT_WIP, projectId: ts.project.id,
debit: laborCost, credit: 0 },
{ accountId: ACCOUNTS.ACCRUED_LABOR,
debit: 0, credit: laborCost },
],
});
ts.status = 'approved';
await this.tsRepo.save(ts);
}The project module is built with NestJS for the API, PostgreSQL for data, and React for the frontend. The project dashboard uses Recharts to display budget vs. actual trend lines over time. We implemented a traffic-light system: green if actuals are below 75% of budget, amber if 75-90%, red if above 90%. This visual alert system means project managers can see at a glance which projects are trending over budget without reading numbers. The dashboard refreshes every 15 minutes via React Query's refetchInterval.
Staff cost rates (salary / productive hours) are sensitive payroll information. Do not display them on timesheet screens or in reports that project managers or clients can access. Store cost rates in the HR module with RBAC restricting access to Finance and HR roles only. Project reports should show total project cost without revealing individual staff cost rates. We had a client request that caused us to add a cost_rate_visible permission specifically for this — by default, cost rates are visible only to Finance and above.
The profitability report shows for each project: contracted value, total cost (labor + direct), gross profit, gross margin %, and billing status (how much has been invoiced vs. contracted). For T&M projects, we also show realization — the ratio of billed hours to incurred hours — which flags scope creep and unbillable work. Management uses this to decide which project types to pursue more aggressively and which to reprice or decline.
The project module integrates bidirectionally with the finance module: costs post to the general ledger as they're approved (debit Project WIP, credit Accrued Labor), and revenue posts when invoices are raised. At project completion, the WIP is cleared against revenue. For payroll integration, the timesheet hours serve as the source for computing payroll for hourly or project-basis staff — the payroll module reads approved timesheet hours and generates the payroll computation without manual data re-entry.