ERPNext Project Timesheet Integration with Billing and Invoicing: Complete Technical Guide

ERPNext automatically converts billable time-log rows into Sales Invoice line items, updating billing totals, percentage billed calculations, and document status in real-time through the Timesheet document architecture.

The project timesheet integration with billing and invoicing in ERPNext bridges project management and accounts receivable by treating each Timesheet Detail row as a potential billing line. As implemented in the frappe/erpnext repository, this system calculates rates, aggregates totals, and maintains audit trails linking time entries to specific invoices.

How Timesheet Detail Stores Billable Time Data

The TimesheetDetail document serves as the foundational data structure for time logging and billing eligibility in erpnext/projects/doctype/timesheet_detail/timesheet_detail.py.

Billing Fields and Validation

Each time-log row contains specific fields that control billing behavior:

  • is_billable – Boolean flag determining if hours should be charged to the customer
  • billing_hours – Quantity of hours to invoice (defaults to hours when billable)
  • billing_rate – Unit price fetched from the Activity Type or overridden manually
  • billing_amount – Calculated value (billing_rate * billing_hours)
  • sales_invoice – Reference linking the row to a specific invoice

When a row is saved, the document validates date ranges and automatically copies hours to billing_hours if is_billable is checked (lines 65–74). The update_cost method (lines 84–94) then computes the final billing amount by multiplying the rate against billable hours.

Aggregating Totals at the Timesheet Level

The parent Timesheet document in erpnext/projects/doctype/timesheet/timesheet.py maintains rollup fields that reflect the aggregate financial state of all child rows.

The calculate_total_amounts method (lines 101–115) iterates through the time_logs child table to compute:

  • total_billable_amount – Sum of all billing_amount values where is_billable = 1
  • total_billed_amount – Sum of amounts from rows already linked to a sales_invoice
  • total_billable_hours – Sum of billing_hours for billable rows
  • total_billed_hours – Sum of hours from invoiced rows

Only rows containing a populated sales_invoice field contribute to the "billed" totals, ensuring accurate work-in-progress reporting.

Billing Status Workflow

After totals are computed, the calculate_percentage_billed method derives per_billed, which the set_status method (lines 27–38) translates into discrete workflow states:

  • Draft / Submitted – Initial states before any billing activity
  • Partially Billed – Triggered when per_billed is greater than 0% but less than 100%
  • Billed – Activated when per_billed reaches 100%
  • Completed – Special status when the Timesheet itself is linked to a Sales Invoice via the sales_invoice field

This status progression provides immediate visual indicators of billing progress within project dashboards.

Creating Sales Invoices from Timesheets

The whitelisted function make_sales_invoice (lines 16–62) in timesheet.py generates invoices for unbilled time entries while maintaining referential integrity.

Invoice Generation Logic

The function performs several critical validations before creating the invoice:

  1. Verifies that total_billable_hours exceeds total_billed_hours to prevent duplicate billing
  2. Calculates remaining billing amount and rate for the uninvoiced portion
  3. Creates a new Sales Invoice document, copying the Timesheet's company and project fields
  4. Appends entries to the invoice's timesheets child table, linking back to the original Timesheet Detail records for complete traceability

Post-Submission Updates

After the Sales Invoice is submitted, ERPNext calls calculate_billing_amount_for_timesheet (implemented in erpnext/accounts/doctype/sales_invoice/sales_invoice.py), which updates the sales_invoice field on each linked time log. This update automatically triggers the Timesheet's aggregation methods, refreshing totals and status without manual intervention.

Handling Invoice Cancellations and Returns

When billing errors occur or customers return services, the unlink_sales_invoice method (lines 94–101) handles cleanup operations. This method clears the sales_invoice reference from all related Timesheet Detail rows and recomputes the parent Timesheet's totals and billing status.

This ensures that cancelled invoices properly "release" time entries back to the pool of billable hours, preventing revenue leakage while maintaining audit accuracy.

Timesheet Billing Reporting

The Timesheet Billing Summary report in erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py provides project managers with aggregated billing data. The report queries billing_hours and billing_amount directly from the Timesheet Detail table, enabling analysis by project, employee, or date range.

Practical Implementation Examples

Creating a Timesheet and Generating an Invoice

import frappe
from erpnext.projects.doctype.timesheet.timesheet import make_sales_invoice

# Create a new Timesheet with billable hours

ts = frappe.new_doc("Timesheet")
ts.company = "My Company"
ts.employee = frappe.session.user
ts.append("time_logs", {
    "activity_type": "Development",
    "project": "PROJ-001",
    "from_time": "2026-05-20 09:00:00",
    "to_time": "2026-05-20 12:00:00",
    "is_billable": 1,
})
ts.save()
ts.submit()

# Generate Sales Invoice for unbilled hours

si = make_sales_invoice(
    source_name=ts.name,
    item_code="_Test Service Item",
    customer="_Test Customer",
    currency="USD"
)
si.save()
si.submit()

# Verify updated status

frappe.db.get_value("Timesheet", ts.name, "status")   # Returns "Partially Billed" or "Billed"

Unlinking a Cancelled Sales Invoice

ts = frappe.get_doc("Timesheet", "TS-0001")
ts.unlink_sales_invoice("SINV-0005")
ts.save()

Summary

  • Granular Billing Control: Individual time-log rows (Timesheet Detail) contain is_billable flags and billing rates, allowing precise control over which hours are chargeable.
  • Automatic Aggregation: The Timesheet document automatically sums billable amounts and tracks billed percentages through calculate_total_amounts and set_status.
  • Seamless Invoice Creation: The make_sales_invoice function generates Sales Invoices from unbilled rows while maintaining bidirectional links between time entries and invoices.
  • Reversal Handling: The unlink_sales_invoice method properly clears billing references when invoices are cancelled, ensuring hours return to billable status.
  • Integrated Reporting: The Timesheet Billing Summary report provides project-level visibility into billed versus billable hours.

Frequently Asked Questions

How does ERPNext calculate billing amounts for timesheet entries?

ERPNext calculates billing amounts in the TimesheetDetail class within timesheet_detail.py. The update_cost method (lines 84–94) multiplies billing_hours by billing_rate to derive billing_amount. When a row is marked billable, the system automatically copies the logged hours to billing_hours unless manually overridden.

What happens to a Timesheet when a linked Sales Invoice is cancelled?

When a Sales Invoice is cancelled or a Sales Return is processed, ERPNext calls the unlink_sales_invoice method (lines 94–101) in timesheet.py. This clears the sales_invoice field from all associated time-log rows and triggers recalculation of total_billed_amount and per_billed, typically reverting the Timesheet status from "Billed" to "Partially Billed" or "Submitted."

What is the difference between Partially Billed and Completed status?

Partially Billed indicates that some but not all billable rows have been linked to Sales Invoices (0% < per_billed < 100%). Completed status occurs only when the Timesheet document itself is linked to a Sales Invoice via the sales_invoice field on the parent document, effectively treating the entire timesheet as a single line item rather than billing individual time logs.

Which report shows billing summary by project?

The Timesheet Billing Summary report located in erpnext/projects/report/timesheet_billing_summary/timesheet_billing_summary.py aggregates billing_hours and billing_amount from Timesheet Detail records. This report allows filtering and grouping by project, employee, or date range to analyze realized revenue against logged time.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →