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

> Automate billing with ERPNext Timesheet integration. Convert time logs to sales invoices, update billing totals, and track progress in real-time. A complete technical guide.

- Repository: [Frappe/erpnext](https://github.com/frappe/erpnext)
- Tags: how-to-guide
- Published: 2026-05-20

---

**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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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

```python
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

```python
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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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`](https://github.com/frappe/erpnext/blob/main/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.