Skip to main content
BlogERP

Project Costing and Time Tracking in a Custom ERP: Budgets, Timesheets, and Profitability

How to build a project costing module with time tracking, cost allocation, budget monitoring, and profitability reporting in a custom ERP using NestJS and PostgreSQL.

Matthews Wong

Software Engineer

June 202510 min read
ERP

Key Takeaway

Project costing only works when timesheets, purchases and revenue all post against the same project code, so profitability can be read per project rather than estimated at year end. For professional services and construction firms that number is the difference between pricing the next job well or badly.

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. As a freelance ERP consultant, 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.

The Project Budget Structure

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.

Timesheet Entry and Approval

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 in my freelance ERP consulting work: 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.

Expense Claims Integration

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.

Multiple Billing Models

The system I built 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);
}

How I Approached This

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. I 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. I had a client request that caused me to add a cost_rate_visible permission specifically for this — by default, cost rates are visible only to Finance and above.

Project Profitability Reporting

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, the report also shows 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.

Integration with Finance and Payroll

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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Why does the timesheet approval flow automatically post labor costs to the project journal?

Once a project manager approves a timesheet, the system multiplies each staff member's approved hours by their cost rate stored in the HR module and posts the result as a debit to the project cost account. This automation eliminates the error-prone manual calculation of hours spent and their cost that typically plagues professional services firms. No separate data entry step is required — the cost flows directly from approval to the general ledger.

How does the traffic-light budget monitoring system work?

The project dashboard displays a traffic-light indicator based on how much of the budget has been consumed: green when actuals are below 75% of the budget, amber between 75–90%, and red above 90%. This visual alert lets project managers see at a glance which projects are trending over budget without reading detailed numbers. The dashboard refreshes every 15 minutes via React Query's refetchInterval.

What are the three billing models supported, and how do they affect invoice generation?

The system supports Time and Materials (T&M), Fixed Price, and Cost Plus. For T&M projects, draft AR invoices are generated automatically from approved timesheets and expense claims. Fixed Price projects trigger invoices on project milestones, while Cost Plus invoices are calculated as total cost multiplied by one plus the agreed margin percentage. The billing model is set at project creation and determines which invoice generation logic runs.

Why must staff cost rates be kept hidden from project managers and clients?

Staff cost rates are derived from salary data and are sensitive payroll information. Exposing them on timesheet screens or project reports would reveal individual compensation details to people who should not have that access. The system stores cost rates in the HR module with RBAC restricting visibility to Finance and HR roles only, and project reports show total project cost without breaking down individual rates. A dedicated cost_rate_visible permission was added specifically to enforce this boundary.

How does the billable vs. non-billable distinction help management identify inefficiency?

Every timesheet entry captures whether the time is billable or non-billable, even when worked on a client project. Time spent on internal meetings, proposal writing, and rework counts as non-billable. The ratio of billable to total hours per project manager is one of the most revealing metrics in the system — it surfaces who is efficient and who is spending excessive time on overhead, and clients are consistently surprised by their actual billability rates when they first see this data.