# How to Configure Tax Withholding Rules and TDS Deductions in ERPNext

> Learn to configure TDS deductions in ERPNext. Set up withholding rules, categories, and apply TDS on invoices and payments for automatic calculation and GL postings.

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

---

**To configure TDS deductions in ERPNext, define a Tax Withholding Group and Rate to establish applicable percentages and thresholds, create a Tax Withholding Category linked to a liability ledger account, and enable the "Apply TDS" checkbox on qualifying purchase invoices or payment entries to trigger automatic withholding calculations and GL postings.**

Learning how to configure tax withholding rules and TDS deductions in the frappe/erpnext repository requires understanding the hierarchical relationship between master data records and the controller framework that automates tax calculation. ERPNext implements Tax-Deducted-at-Source (TDS) and Tax-Collected-at-Source (TCS) through specialized doctypes housed under `erpnext/accounts`, which automatically evaluate thresholds, generate withholding entries, and post to designated liability accounts during transaction validation.

## Master Data Architecture

ERPNext stores withholding configuration in four interlinked doctypes that establish rates, thresholds, ledger accounts, and transaction history.

The **Tax Withholding Group** ([`erpnext/accounts/doctype/tax_withholding_group/tax_withholding_group.py`](https://github.com/frappe/erpnext/blob/main/erpnext/accounts/doctype/tax_withholding_group/tax_withholding_group.py)) serves as a logical container for related rates, such as "TDS 30% Contractors". This group links to **Tax Withholding Rate** records ([`tax_withholding_rate.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_rate.py)) that store the actual percentage, effective date ranges (`from_date`, `to_date`), and threshold limits (`single_threshold`, `cumulative_threshold`).

The **Tax Withholding Category** ([`tax_withholding_category.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_category.py)) binds these rate definitions to specific General Ledger accounts through its child table **Tax Withholding Account**. This category doctype controls critical calculation parameters including `tax_deduction_basis` (Gross vs Net Total), `tax_on_excess_amount` logic, and rounding options. Finally, the system generates **Tax Withholding Entry** records ([`tax_withholding_entry.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_entry.py)) to track each withholding transaction, storing fields like `taxable_amount`, `withholding_amount`, and `status` (Settled, Under Withheld, or Cancelled).

## Step-by-Step Configuration Guide

### Create a Tax Withholding Group

Navigate to Tax Withholding Group and create a new record defining the logical grouping for your TDS rule. The `group_name` field identifies the rate cluster, such as "TDS 30% Contractors" or "TCS 1% Sales".

### Define Tax Withholding Rates and Thresholds

Within the Tax Withholding Rate doctype, create records linked to your group that specify:
- `tax_withholding_rate`: The percentage to withhold (e.g., 30)
- `from_date` and `to_date`: The validity period for the rate
- `single_threshold`: Minimum amount per transaction to trigger withholding (e.g., 5000)
- `cumulative_threshold`: Annual limit that triggers withholding when exceeded across multiple transactions (e.g., 25000)

According to [`erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.py`](https://github.com/frappe/erpnext/blob/main/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.py), the system selects the applicable rate based on the transaction's posting date and evaluates both single-transaction and cumulative limits during validation.

### Configure the Tax Withholding Category

Create a Tax Withholding Category that references your group and defines calculation behavior:
- Set `tax_deduction_basis` to "Gross Total" or "Net Total" to determine the base amount
- Enable `round_off_tax_amount` for whole-number rounding
- In the **Tax Withholding Account** child table, link the liability account (e.g., "TDS Payable - XYZ") that will receive withheld amounts
- Set `disable_cumulative_threshold` or `disable_transaction_threshold` to exempt specific threshold types

The category validation logic in [`tax_withholding_category.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_category.py) ensures that linked accounts are valid liability accounts and that date ranges do not overlap improperly.

### Enable TDS on Transactions

To activate withholding on a Purchase Invoice:
1. Check the **Apply TDS** checkbox at the document level
2. In the Items table, enable **Apply TDS** for specific lines and select the **Tax Withholding Category**
3. Save the document to trigger the `PurchaseTaxWithholding` controller

The controller, defined in [`tax_withholding_entry.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_entry.py), automatically calculates item-wise taxable amounts, evaluates thresholds against historic data, and inserts tax rows that deduct from the supplier balance while crediting your TDS liability account.

### Optional: Configure Lower Deduction Certificates

For suppliers with valid Lower Deduction Certificates (LDCs), create an LDC record linked to the supplier and Tax Withholding Category. The controller method `get_valid_ldc_records` automatically detects these certificates and applies the reduced rate to the `ldc_unutilized_amount` before calculating the final withholding.

## Technical Execution Flow

When a document containing `apply_tds = 1` is validated, the `TaxWithholdingController.on_validate` method executes the following logic from [`erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py`](https://github.com/frappe/erpnext/blob/main/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py):

1. **Eligibility Check**: `_is_tax_withholding_applicable` verifies the document type and party configuration
2. **Category Resolution**: `TaxWithholdingDetails` class fetches applicable rates via `get_applicable_tax_row` and loads valid LDCs
3. **Threshold Evaluation**: `_evaluate_thresholds` compares the transaction amount against `single_threshold` and cumulative totals, respecting the `ignore_tax_withholding_threshold` flag when present
4. **Entry Generation**: `_generate_withholding_entries` creates **Tax Withholding Entry** records, merging historic under-withheld amounts and applying "tax on excess amount" logic if configured
5. **Tax Row Updates**: `update_tax_rows` calculates account-wise totals and inserts rows into the document's taxes table with `Add/Deduct` set to "Deduct" for suppliers (TDS) or "Add" for customers (TCS)
6. **Finalization**: `_process_withholding_entries` validates totals and sets entry statuses to "Settled" or "Under Withheld"

Subclasses like `PurchaseTaxWithholding`, `SalesTaxWithholding`, `PaymentTaxWithholding`, and `JournalTaxWithholding` override taxable amount calculations to handle item-wise sums, unallocated payment amounts, and multi-party journal entries respectively.

## Programmatic Configuration with Python

You can automate master data creation through the Frappe console or server scripts:

```python
import frappe

# Create Tax Withholding Group

group = frappe.get_doc({
    "doctype": "Tax Withholding Group",
    "group_name": "TDS 30% Contractors"
}).insert()

# Create Rate with Thresholds

rate = frappe.get_doc({
    "doctype": "Tax Withholding Rate",
    "tax_withholding_group": group.name,
    "tax_withholding_rate": 30,
    "from_date": "2024-04-01",
    "to_date": "2025-03-31",
    "single_threshold": 5000,
    "cumulative_threshold": 25000
}).insert()

# Create Category with Ledger Account

category = frappe.get_doc({
    "doctype": "Tax Withholding Category",
    "category_name": "Contractor TDS",
    "tax_deduction_basis": "Gross Total",
    "round_off_tax_amount": 1,
    "tax_withholding_group": group.name,
    "tax_withholding_account": [{
        "account": "TDS Payable - XYZ"
    }]
}).insert()

```

To simulate a purchase invoice that triggers automatic withholding:

```python
pi = frappe.get_doc({
    "doctype": "Purchase Invoice",
    "supplier": "Acme Contractors",
    "company": "My Company",
    "posting_date": "2024-06-15",
    "apply_tds": 1,
    "items": [{
        "item_code": "Consulting Service",
        "qty": 1,
        "rate": 10000,
        "apply_tds": 1,
        "tax_withholding_category": "Contractor TDS"
    }]
})
pi.insert()
pi.submit()

```

After submission, verify generated entries using:

```python
entries = frappe.get_all(
    "Tax Withholding Entry",
    filters={"taxable_name": pi.name, "docstatus": 1},
    fields=["name", "status", "tax_rate", "taxable_amount", "withholding_amount"]
)

```

## Summary

- **Tax Withholding Group** organizes related rates, while **Tax Withholding Rate** stores percentages, date ranges, and threshold values in `erpnext/accounts/doctype/tax_withholding_rate/`
- **Tax Withholding Category** links rates to GL accounts and controls calculation logic including exemptions and rounding through [`tax_withholding_category.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_category.py)
- The `TaxWithholdingController` hierarchy (Purchase, Sales, Payment, Journal) automates withholding calculations during document validation via `on_validate` hooks
- Enable TDS by checking **Apply TDS** on both the document header and applicable item rows, then selecting the appropriate category
- The system automatically creates **Tax Withholding Entry** records to track taxable amounts, withheld taxes, and settlement status against configured thresholds

## Frequently Asked Questions

### What is the difference between single threshold and cumulative threshold in ERPNext TDS configuration?

The `single_threshold` defines the minimum transaction amount required to trigger TDS withholding on a single invoice, while the `cumulative_threshold` sets an annual or periodic limit across multiple transactions with the same party. When the cumulative total exceeds this threshold, the system withholds tax on subsequent transactions even if individual amounts fall below the single transaction limit, as implemented in the `_evaluate_thresholds` method of the withholding controller.

### How does ERPNext handle Lower Deduction Certificates (LDC) during TDS calculation?

The controller calls `get_valid_ldc_records` to fetch active certificates for the supplier and category, then applies the reduced rate to the `ldc_unutilized_amount` field before calculating the final withholding amount. If the invoice amount exceeds the remaining LDC balance, the system applies the reduced rate to the certificate balance and the standard rate to the excess amount, ensuring compliance while maximizing certificate utilization.

### Which documents support automatic TDS deduction in ERPNext?

According to the source code in [`tax_withholding_entry.py`](https://github.com/frappe/erpnext/blob/main/tax_withholding_entry.py), the `TaxWithholdingController` supports Purchase Invoice (via `PurchaseTaxWithholding`), Sales Invoice for TCS (via `SalesTaxWithholding`), Payment Entry (via `PaymentTaxWithholding`), and Journal Entry (via `JournalTaxWithholding`). Each subclass implements specific logic for deriving taxable amounts, such as item-wise totals for invoices or unallocated amounts for payments.

### Where can I verify which TDS rates will apply to a specific posting date?

Use the **Tax Withholding Details** report located at [`erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py`](https://github.com/frappe/erpnext/blob/main/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py) to preview applicable rates, thresholds, and account mappings for any given date. This report queries the same `get_applicable_tax_row` logic used by the controller, ensuring the displayed configuration matches what the system will apply during transaction processing.