From e22326065d0e20fe87baed0929e4643f85ae4347 Mon Sep 17 00:00:00 2001 From: Lakshit Jain Date: Wed, 22 Apr 2026 00:02:19 +0530 Subject: [PATCH] feat: enhance tax withholding details report with additional columns support (backport #54409) (#54432) --- .../tax_withholding_details.py | 66 ++++++++++++++--- .../test_tax_withholding_details.py | 40 ++++++++++- .../tds_computation_summary.py | 70 +++++++++++-------- 3 files changed, 135 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index d93c60b2cf4..ea6a07b5f16 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -11,6 +11,10 @@ from erpnext.accounts.utils import get_currency_precision def execute(filters=None): + return _execute(filters) + + +def _execute(filters=None, additional_table_columns=None): if filters.get("party_type") == "Customer": party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name") else: @@ -25,9 +29,9 @@ def execute(filters=None): net_total_map, ) = get_tds_docs(filters) - columns = get_columns(filters) + columns = get_columns(filters, additional_table_columns) - res = get_result(filters, tds_accounts, tax_category_map, net_total_map) + res = get_result(filters, tds_accounts, tax_category_map, net_total_map, additional_table_columns) return columns, res @@ -38,12 +42,14 @@ def validate_filters(filters): frappe.throw(_("From Date must be before To Date")) -def get_result(filters, tds_accounts, tax_category_map, net_total_map): +def get_result(filters, tds_accounts, tax_category_map, net_total_map, additional_table_columns=None): party_names = {v.party for v in net_total_map.values() if v.party} party_map = get_party_pan_map(filters.get("party_type"), party_names) tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(net_total_map) precision = get_currency_precision() + twc = get_tax_withholding_category_details(additional_table_columns) + twc_additional_columns = _get_twc_additional_columns(additional_table_columns) entries = {} for (voucher_type, name), details in gle_map.items(): @@ -119,8 +125,8 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map): row.update( { - "section_code": tax_withholding_category or "", - "entity_type": party_map.get(party, {}).get(party_type), + "tax_withholding_category": tax_withholding_category or "", + "party_entity_type": party_map.get(party, {}).get(party_type), "rate": rate, "total_amount": total_amount, "grand_total": grand_total, @@ -135,17 +141,47 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map): } ) + if tax_withholding_category: + if twc_details := twc.get(tax_withholding_category, {}): + for col in twc_additional_columns or []: + row[col] = twc_details.get(col) + key = entry.voucher_no if key in entries: entries[key]["tax_amount"] += tax_amount else: entries[key] = row out = list(entries.values()) - out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"])) + out.sort(key=lambda x: (x["tax_withholding_category"], x["transaction_date"], x["ref_no"])) return out +def get_tax_withholding_category_details(additional_table_columns=None): + if not additional_table_columns: + return {} + + category_fields = _get_twc_additional_columns(additional_table_columns) + + if not category_fields: + return {} + + rows = frappe.get_all("Tax Withholding Category", fields=["name", *category_fields]) + + return {row["name"]: row for row in rows} + + +def _get_twc_additional_columns(additional_table_columns): + if not additional_table_columns: + return [] + + return [ + col.get("fieldname") + for col in additional_table_columns + if col.get("_doctype") == "Tax Withholding Category" and col.get("fieldname") + ] + + def get_party_pan_map(party_type, party_names): party_map = frappe._dict() @@ -201,19 +237,22 @@ def get_gle_map(net_total_map): return gle_map -def get_columns(filters): +def get_columns(filters, additional_table_columns=None): pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id" columns = [ { - "label": _("Section Code"), + "label": _("Tax Withholding Category"), "options": "Tax Withholding Category", - "fieldname": "section_code", + "fieldname": "tax_withholding_category", "fieldtype": "Link", - "width": 90, + "width": 180, }, {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60}, ] + if additional_table_columns: + columns.extend(additional_table_columns) + if filters.naming_series == "Naming Series": columns.append( { @@ -236,7 +275,12 @@ def get_columns(filters): columns.extend( [ - {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100}, + { + "label": _(f"{filters.get('party_type', 'Party')} Type"), + "fieldname": "party_entity_type", + "fieldtype": "Data", + "width": 100, + }, ] ) if filters.party_type == "Supplier": diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index 56dba9d86d3..4662a2d7b51 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -11,7 +11,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import ( create_tax_withholding_category, ) -from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import execute +from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import _execute, execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year @@ -112,13 +112,49 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): ] self.check_expected_values(result, expected_values) + def test_additional_tax_withholding_category_column(self): + tds_category = "TDS - Additional Column" + create_tax_category(tds_category, rate=10, account="TDS - _TC", cumulative_threshold=1) + inv = make_purchase_invoice(rate=1000, do_not_submit=True) + inv.apply_tds = 1 + inv.tax_withholding_category = tds_category + inv.submit() + + field_name = "category_name" + expected_value = "Additional Column Display Name" + frappe.db.set_value("Tax Withholding Category", tds_category, field_name, expected_value) + + additional_table_columns = [ + { + "label": "Category Name", + "fieldname": field_name, + "fieldtype": "Data", + "width": 140, + "_doctype": "Tax Withholding Category", + } + ] + + filters = frappe._dict( + company="_Test Company", + party_type="Supplier", + from_date=today(), + to_date=today(), + ) + + columns, data = _execute(filters, additional_table_columns) + + self.assertTrue(any(col.get("fieldname") == field_name for col in columns)) + invoice_row = next((row for row in data if row.get("ref_no") == inv.name), None) + self.assertIsNotNone(invoice_row) + self.assertEqual(invoice_row.get(field_name), expected_value) + def check_expected_values(self, result, expected_values): for i in range(len(result)): voucher = frappe._dict(result[i]) voucher_expected_values = expected_values[i] voucher_actual_values = ( voucher.ref_no, - voucher.section_code, + voucher.tax_withholding_category, voucher.rate, voucher.base_tax_withholding_net_total, voucher.base_total, diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index cbceaeed092..2a4eaf841e5 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -2,6 +2,7 @@ import frappe from frappe import _ from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import ( + _get_twc_additional_columns, get_result, get_tds_docs, ) @@ -9,6 +10,10 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): + return _execute(filters) + + +def _execute(filters=None, additional_table_columns=None): if filters.get("party_type") == "Customer": party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name") else: @@ -18,15 +23,15 @@ def execute(filters=None): validate_filters(filters) - columns = get_columns(filters) + columns = get_columns(filters, additional_table_columns) ( tds_accounts, tax_category_map, net_total_map, ) = get_tds_docs(filters) - res = get_result(filters, tds_accounts, tax_category_map, net_total_map) - final_result = group_by_party_and_category(res, filters) + res = get_result(filters, tds_accounts, tax_category_map, net_total_map, additional_table_columns) + final_result = group_by_party_and_category(res, filters, additional_table_columns) return columns, final_result @@ -44,32 +49,33 @@ def validate_filters(filters): filters["fiscal_year"] = from_year -def group_by_party_and_category(data, filters): +def group_by_party_and_category(data, filters, additional_table_columns=None): party_category_wise_map = {} + twc_additional_columns = _get_twc_additional_columns(additional_table_columns) for row in data: - party_category_wise_map.setdefault( - (row.get("party"), row.get("section_code")), - { - "pan": row.get("pan"), - "tax_id": row.get("tax_id"), - "party": row.get("party"), - "party_name": row.get("party_name"), - "section_code": row.get("section_code"), - "entity_type": row.get("entity_type"), - "rate": row.get("rate"), - "total_amount": 0.0, - "tax_amount": 0.0, - }, - ) + key = (row.get("party"), row.get("tax_withholding_category")) + default_row = { + "pan": row.get("pan"), + "tax_id": row.get("tax_id"), + "party": row.get("party"), + "party_name": row.get("party_name"), + "tax_withholding_category": row.get("tax_withholding_category"), + "party_entity_type": row.get("party_entity_type"), + "rate": row.get("rate"), + "total_amount": 0.0, + "tax_amount": 0.0, + } - party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get( - "total_amount", 0.0 - ) + if twc_additional_columns: + for col in twc_additional_columns: + default_row[col] = row.get(col) - party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get( - "tax_amount", 0.0 - ) + party_category_wise_map.setdefault(key, default_row) + + party_category_wise_map.get(key)["total_amount"] += row.get("total_amount", 0.0) + + party_category_wise_map.get(key)["tax_amount"] += row.get("tax_amount", 0.0) final_result = get_final_result(party_category_wise_map) @@ -84,7 +90,7 @@ def get_final_result(party_category_wise_map): return out -def get_columns(filters): +def get_columns(filters, additional_table_columns=None): pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id" columns = [ {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90}, @@ -107,16 +113,24 @@ def get_columns(filters): } ) + if additional_table_columns: + columns.extend(additional_table_columns) + columns.extend( [ { - "label": _("Section Code"), + "label": _("Tax Withholding Category"), "options": "Tax Withholding Category", - "fieldname": "section_code", + "fieldname": "tax_withholding_category", "fieldtype": "Link", "width": 180, }, - {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180}, + { + "label": _(f"{filters.get('party_type', 'Party')} Type"), + "fieldname": "party_entity_type", + "fieldtype": "Data", + "width": 180, + }, { "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"), "fieldname": "rate",