From b9fb1045d7f4331c064c491072199f65376159af Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 6 Nov 2022 11:50:20 +0530 Subject: [PATCH] feat: Item Wise TDS Calculation --- .../purchase_invoice/purchase_invoice.json | 25 +++++++++++- .../purchase_invoice_item.json | 7 ++++ .../tax_withholding_category.py | 32 +++++++++------ .../test_tax_withholding_category.py | 40 +++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 13 ++++++ erpnext/patches.txt | 2 +- erpnext/patches/v14_0/update_tds_fields.py | 29 ++++++++++++++ erpnext/public/js/controllers/transaction.js | 4 +- 8 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 erpnext/patches/v14_0/update_tds_fields.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index e73d6023328..370c0fc9605 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -57,6 +57,8 @@ "column_break_28", "total", "net_total", + "tax_withholding_net_total", + "base_tax_withholding_net_total", "taxes_section", "taxes_and_charges", "column_break_58", @@ -89,7 +91,6 @@ "section_break_44", "apply_discount_on", "base_discount_amount", - "additional_discount_account", "column_break_46", "additional_discount_percentage", "discount_amount", @@ -1421,6 +1422,26 @@ "label": "Is Old Subcontracting Flow", "read_only": 1 }, + { + "default": "0", + "fieldname": "tax_withholding_net_total", + "fieldtype": "Currency", + "hidden": 1, + "label": "Tax Withholding Net Total", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "base_tax_withholding_net_total", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Tax Withholding Net Total", + "no_copy": 1, + "options": "currency", + "print_hide": 1, + "read_only": 1 + }, { "collapsible_depends_on": "tax_withheld_vouchers", "fieldname": "tax_withheld_vouchers_section", @@ -1519,7 +1540,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-10-11 13:04:44.304389", + "modified": "2022-11-04 01:02:44.544878", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index a8f6f80b6b9..5c1cb0dcc68 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -40,6 +40,7 @@ "discount_amount", "base_rate_with_margin", "sec_break2", + "apply_tds", "rate", "amount", "item_tax_template", @@ -868,6 +869,12 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "default": "1", + "fieldname": "apply_tds", + "fieldtype": "Check", + "label": "Apply TDS" } ], "idx": 1, diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 7eddd81ee03..30ed91b9744 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -61,6 +61,9 @@ def get_party_details(inv): def get_party_tax_withholding_details(inv, tax_withholding_category=None): + if inv.doctype == "Payment Entry": + inv.tax_withholding_net_total = inv.net_total + pan_no = "" parties = [] party_type, party = get_party_details(inv) @@ -242,7 +245,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if party_type == "Supplier": ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: - net_total = inv.net_total + net_total = inv.tax_withholding_net_total if ldc: tax_amount = get_tds_amount_from_ldc( ldc, parties, pan_no, tax_details, posting_date, net_total @@ -272,6 +275,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + field = ( + "base_tax_withholding_net_total as base_net_total" + if party_type == "Supplier" + else "base_net_total" + ) voucher_wise_amount = {} vouchers = [] @@ -288,7 +296,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"]) + invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field]) for d in invoices_details: vouchers.append(d.name) @@ -392,7 +400,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): tds_amount = 0 invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} - field = "sum(net_total)" + field = "sum(tax_withholding_net_total)" if cint(tax_details.consider_party_ledger_amount): invoice_filters.pop("apply_tds", None) @@ -415,12 +423,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ) supp_credit_amt += supp_jv_credit_amt - supp_credit_amt += inv.net_total + supp_credit_amt += inv.tax_withholding_net_total threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) - if (threshold and inv.net_total >= threshold) or ( + if (threshold and inv.tax_withholding_net_total >= threshold) or ( cumulative_threshold and supp_credit_amt >= cumulative_threshold ): if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( @@ -428,11 +436,11 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ): # Get net total again as TDS is calculated on net total # Grand is used to just check for threshold breach - net_total = 0 - if vouchers: - net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") - - net_total += inv.net_total + net_total = ( + frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") + or 0.0 + ) + net_total += inv.tax_withholding_net_total supp_credit_amt = net_total - cumulative_threshold if ldc and is_valid_certificate( @@ -440,7 +448,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ldc.valid_upto, inv.get("posting_date") or inv.get("transaction_date"), tax_deducted, - inv.net_total, + inv.tax_withholding_net_total, ldc.certificate_limit, ): tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) @@ -523,7 +531,7 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net limit_consumed = frappe.db.get_value( "Purchase Invoice", {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, - "sum(net_total)", + "sum(tax_withholding_net_total)", ) if is_valid_certificate( diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index e80fe11ab30..40c732bae52 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -186,6 +186,46 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in reversed(invoices): d.cancel() + def test_tds_calculation_on_net_total_partial_tds(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" + ) + invoices = [] + + pi = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000, do_not_save=True) + pi.extend( + "items", + [ + { + "doctype": "Purchase Invoice Item", + "item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"), + "qty": 1, + "rate": 20000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + "apply_tds": 0, + }, + { + "doctype": "Purchase Invoice Item", + "item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"), + "qty": 1, + "rate": 35000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + "apply_tds": 1, + }, + ], + ) + pi.save() + pi.submit() + invoices.append(pi) + + self.assertEqual(pi.taxes[0].tax_amount, 5500) + + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + def test_multi_category_single_supplier(self): frappe.db.set_value( "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category" diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b5836c9070a..81de6823782 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -58,12 +58,25 @@ class calculate_taxes_and_totals(object): self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() + self.calculate_tax_withholding_net_total() self.calculate_taxes() self.manipulate_grand_total_for_inclusive_tax() self.calculate_totals() self._cleanup() self.calculate_total_net_weight() + def calculate_tax_withholding_net_total(self): + if hasattr(self.doc, "tax_withholding_net_total"): + sum_net_amount = 0 + sum_base_net_amount = 0 + for item in self.doc.get("items"): + if hasattr(item, "apply_tds") and item.apply_tds: + sum_net_amount += item.net_amount + sum_base_net_amount += item.base_net_amount + + self.doc.tax_withholding_net_total = sum_net_amount + self.doc.base_tax_withholding_net_total = sum_base_net_amount + def validate_item_tax_template(self): for item in self.doc.get("items"): if item.item_code and item.get("item_tax_template"): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5ed8f3f4a91..8e726bf0c0a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -317,4 +317,4 @@ erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v13_0.update_schedule_type_in_loans - +erpnext.patches.v14_0.update_tds_fields \ No newline at end of file diff --git a/erpnext/patches/v14_0/update_tds_fields.py b/erpnext/patches/v14_0/update_tds_fields.py new file mode 100644 index 00000000000..a333c5d7a50 --- /dev/null +++ b/erpnext/patches/v14_0/update_tds_fields.py @@ -0,0 +1,29 @@ +import frappe +from frappe.utils import nowdate + +from erpnext.accounts.utils import FiscalYearError, get_fiscal_year + + +def execute(): + # Only do for current fiscal year, no need to repost for all years + for company in frappe.get_all("Company"): + try: + fiscal_year_details = get_fiscal_year(date=nowdate(), company=company.name, as_dict=True) + + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + + frappe.qb.update(purchase_invoice).set( + purchase_invoice.tax_withholding_net_total, purchase_invoice.net_total + ).set( + purchase_invoice.base_tax_withholding_net_total, purchase_invoice.base_net_total + ).where( + purchase_invoice.company == company.name + ).where( + purchase_invoice.apply_tds == 1 + ).where( + purchase_invoice.posting_date >= fiscal_year_details.year_start_date + ).where( + purchase_invoice.docstatus == 1 + ).run() + except FiscalYearError: + pass diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 677ca78829c..46ac80895cf 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1207,7 +1207,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "base_rounding_adjustment"], company_currency); this.frm.set_currency_labels(["total", "net_total", "total_taxes_and_charges", "discount_amount", - "grand_total", "taxes_and_charges_added", "taxes_and_charges_deducted", + "grand_total", "taxes_and_charges_added", "taxes_and_charges_deducted","tax_withholding_net_total", "rounded_total", "in_words", "paid_amount", "write_off_amount", "operating_cost", "scrap_material_cost", "rounding_adjustment", "raw_material_cost", "total_cost"], this.frm.doc.currency); @@ -1224,7 +1224,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } // toggle fields - this.frm.toggle_display(["conversion_rate", "base_total", "base_net_total", + this.frm.toggle_display(["conversion_rate", "base_total", "base_net_total", "base_tax_withholding_net_total", "base_total_taxes_and_charges", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", "base_grand_total", "base_rounded_total", "base_in_words", "base_discount_amount", "base_paid_amount", "base_write_off_amount", "base_operating_cost", "base_raw_material_cost",