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 customerbilling_hours– Quantity of hours to invoice (defaults tohourswhen billable)billing_rate– Unit price fetched from the Activity Type or overridden manuallybilling_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 allbilling_amountvalues whereis_billable = 1total_billed_amount– Sum of amounts from rows already linked to asales_invoicetotal_billable_hours– Sum ofbilling_hoursfor billable rowstotal_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_billedis greater than 0% but less than 100% - Billed – Activated when
per_billedreaches 100% - Completed – Special status when the Timesheet itself is linked to a Sales Invoice via the
sales_invoicefield
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:
- Verifies that
total_billable_hoursexceedstotal_billed_hoursto prevent duplicate billing - Calculates remaining billing amount and rate for the uninvoiced portion
- Creates a new Sales Invoice document, copying the Timesheet's
companyandprojectfields - Appends entries to the invoice's
timesheetschild table, linking back to the originalTimesheet Detailrecords 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) containis_billableflags and billing rates, allowing precise control over which hours are chargeable. - Automatic Aggregation: The
Timesheetdocument automatically sums billable amounts and tracks billed percentages throughcalculate_total_amountsandset_status. - Seamless Invoice Creation: The
make_sales_invoicefunction generates Sales Invoices from unbilled rows while maintaining bidirectional links between time entries and invoices. - Reversal Handling: The
unlink_sales_invoicemethod 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →