From 6a66ce5a973792a6e3585a95508232d042e6528d Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 27 Nov 2025 13:40:23 +0530 Subject: [PATCH] fix: add is_tax_withholding_account field to JournalEntryAccount to avoid recursive tds --- .../doctype/journal_entry/journal_entry.py | 198 +++++++++++------- .../journal_entry_account.json | 10 +- .../journal_entry_account.py | 1 + .../test_tax_withholding_category.py | 10 +- 4 files changed, 143 insertions(+), 76 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index f114095d0ab..b828bcbdbe9 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -354,15 +354,14 @@ class JournalEntry(AccountsController): ) def apply_tax_withholding(self): - from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map - if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"): return party = None party_type = None - party_row = None party_account = None + party_row = None + existing_tds_rows = [] for row in self.get("accounts"): if row.party_type in ("Customer", "Supplier") and row.party: @@ -375,64 +374,119 @@ class JournalEntry(AccountsController): party_account = row.account party_row = row + if row.get("is_tax_withholding_account"): + existing_tds_rows.append(row) + if not party: return - # debit or credit based on party type + party = party + party_type = party_type + party_account = party_account + party_row = party_row + dr_cr = "credit" if party_type == "Supplier" else "debit" rev_dr_cr = "debit" if party_type == "Supplier" else "credit" - precision = self.precision(dr_cr, party_row) + self._reset_existing_tds_rows(party_row, existing_tds_rows, dr_cr, rev_dr_cr, precision) + + net_total = self._calculate_tds_net_total(dr_cr, rev_dr_cr, party_account, precision) + if net_total <= 0: + return + + tds_details = get_party_tax_withholding_details( + frappe._dict( + { + "party_type": party_type, + "party": party, + "doctype": self.doctype, + "company": self.company, + "posting_date": self.posting_date, + "tax_withholding_net_total": net_total, + "base_tax_withholding_net_total": net_total, + "grand_total": net_total, + } + ), + self.tax_withholding_category, + ) + + if not tds_details or not tds_details.get("tax_amount"): + return + + tax_row = self._update_or_create_tds_row(tds_details, precision) + self._adjust_party_row_for_tds(party_row, tds_details, dr_cr, rev_dr_cr, precision) + self._remove_duplicate_tds_rows(tax_row) + + self.set_amounts_in_company_currency() + self.set_total_debit_credit() + self.set_against_account() + + def _reset_existing_tds_rows(self, party_row, existing_tds_rows, dr_cr, rev_dr_cr, precision): + for row in existing_tds_rows: + # Get the TDS amount from the row (TDS is always in credit) + tds_amount = flt(row.get("credit") - row.get("debit"), precision) + if not tds_amount: + continue + + tds_amount_in_party_currency = flt(tds_amount / party_row.get("exchange_rate", 1), precision) + + party_field = dr_cr if party_row.get(dr_cr) else rev_dr_cr + party_field_in_account_currency = f"{party_field}_in_account_currency" + + # For Supplier (dr_cr=credit): add back to credit + # For Customer (dr_cr=debit): subtract from debit (since TDS was added) + multiplier = 1 if dr_cr == "credit" else -1 + tds_amount *= multiplier + tds_amount_in_party_currency *= multiplier + + party_row.update( + { + party_field: flt(party_row.get(party_field) + tds_amount, precision), + party_field_in_account_currency: flt( + party_row.get(party_field_in_account_currency) + tds_amount_in_party_currency, + precision, + ), + } + ) + + row.update( + { + "credit": 0, + "credit_in_account_currency": 0, + "debit": 0, + "debit_in_account_currency": 0, + } + ) + + def _calculate_tds_net_total(self, tds_field, reverse_field, party_account, precision): + from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map + account_type_map = get_account_type_map(self.company) - # net total in company currency. - net_total = flt( + return flt( sum( - d.get(rev_dr_cr) - d.get(dr_cr) + d.get(reverse_field) - d.get(tds_field) for d in self.get("accounts") - if account_type_map.get(d.account) not in ("Tax", "Chargeable") and d.account != party_account + if account_type_map.get(d.account) not in ("Tax", "Chargeable") + and d.account != party_account + and not d.get("is_tax_withholding_account") ), precision, ) - # only apply tds if net total is positive - if net_total <= 0: - return + def _update_or_create_tds_row(self, tax_details, precision): + tax_account = tax_details.get("account_head") + account_currency = get_account_currency(tax_account) + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + exch_rate = _get_exchange_rate(account_currency, company_currency, self.posting_date) - inv = frappe._dict( - { - "party_type": party_type, - "party": party, - "doctype": self.doctype, - "company": self.company, - "posting_date": self.posting_date, - "tax_withholding_net_total": net_total, - "base_tax_withholding_net_total": net_total, - "grand_total": net_total, - } - ) + tax_amount = flt(tax_details.get("tax_amount"), precision) + tax_amount_in_account_currency = flt(tax_amount / exch_rate, precision) - tax_details = get_party_tax_withholding_details(inv, self.tax_withholding_category) - - if not tax_details: - return - - tax_acc = tax_details.get("account_head") - acc_curr = get_account_currency(tax_acc) - comp_curr = frappe.get_cached_value("Company", self.company, "default_currency") - - exch_rate = _get_exchange_rate(acc_curr, comp_curr, self.posting_date) - - tax_amt_in_comp_curr = flt(tax_details.get("tax_amount"), precision) - tax_amt_in_acc_curr = flt(tax_amt_in_comp_curr / exch_rate, precision) - tax_amt_in_party_curr = flt(tax_amt_in_comp_curr / party_row.get("exchange_rate", 1), precision) - - # Update or create tax account row tax_row = None - for row in self.get("accounts"): - if row.account == tax_acc: + if row.account == tax_account and row.get("is_tax_withholding_account"): tax_row = row break @@ -440,66 +494,64 @@ class JournalEntry(AccountsController): tax_row = self.append( "accounts", { - "account": tax_acc, - "account_currency": acc_curr, + "account": tax_account, + "account_currency": account_currency, "exchange_rate": exch_rate, "cost_center": tax_details.get("cost_center"), - "debit": 0, "credit": 0, - "debit_in_account_currency": 0, "credit_in_account_currency": 0, + "debit": 0, + "debit_in_account_currency": 0, + "is_tax_withholding_account": 1, }, ) - # TDS will always be credit tax_row.update( { - "credit": tax_amt_in_comp_curr, - "credit_in_account_currency": tax_amt_in_acc_curr, + "credit": tax_amount, + "credit_in_account_currency": tax_amount_in_account_currency, "debit": 0, "debit_in_account_currency": 0, } ) - # update party row - party_field = dr_cr + return tax_row - # sometime user may enter amount in opposite field as negative value + def _adjust_party_row_for_tds(self, party_row, tax_details, dr_cr, rev_dr_cr, precision): + tax_amount = flt(tax_details.get("tax_amount"), precision) + tax_amount_in_party_currency = flt(tax_amount / party_row.get("exchange_rate", 1), precision) + + party_field = dr_cr if not party_row.get(party_field): party_field = rev_dr_cr - tax_amt_in_comp_curr *= -1 - tax_amt_in_party_curr *= -1 + tax_amount *= -1 + tax_amount_in_party_currency *= -1 - # for customer amount will be added. if dr_cr == "debit": - tax_amt_in_comp_curr *= -1 - tax_amt_in_party_curr *= -1 + tax_amount *= -1 + tax_amount_in_party_currency *= -1 - party_field_in_acc_curr = f"{party_field}_in_account_currency" - party_amt_in_comp_curr = flt(party_row.get(party_field) - tax_amt_in_comp_curr, precision) - party_amt_in_acc_curr = flt(party_row.get(party_field_in_acc_curr) - tax_amt_in_party_curr, precision) + party_field_in_account_currency = f"{party_field}_in_account_currency" party_row.update( { - party_field: party_amt_in_comp_curr, - party_field_in_acc_curr: party_amt_in_acc_curr, + party_field: flt(party_row.get(party_field) - tax_amount, precision), + party_field_in_account_currency: flt( + party_row.get(party_field_in_account_currency) - tax_amount_in_party_currency, precision + ), } ) - # remove other tds rows if any - dup_rows = [] - for row in self.get("accounts"): - if row.account == tax_acc and row != tax_row: - dup_rows.append(row) + def _remove_duplicate_tds_rows(self, current_tax_row): + rows_to_remove = [ + row + for row in self.get("accounts") + if row.get("is_tax_withholding_account") and row != current_tax_row + ] - for row in dup_rows: + for row in rows_to_remove: self.remove(row) - # recalculate totals - self.set_amounts_in_company_currency() - self.set_total_debit_credit() - self.set_against_account() - def update_asset_value(self): self.update_asset_on_depreciation() self.update_asset_on_disposal() diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index f1832255122..675bfcf86c8 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -34,6 +34,7 @@ "reference_detail_no", "advance_voucher_type", "advance_voucher_no", + "is_tax_withholding_account", "col_break3", "is_advance", "user_remark", @@ -281,12 +282,19 @@ "options": "advance_voucher_type", "read_only": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "is_tax_withholding_account", + "fieldtype": "Check", + "label": "Is Tax Withholding Account", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2025-10-27 13:48:32.805100", + "modified": "2025-11-27 12:23:33.157655", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py index b801ac8c9a5..d26224103c0 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py @@ -28,6 +28,7 @@ class JournalEntryAccount(Document): debit_in_account_currency: DF.Currency exchange_rate: DF.Float is_advance: DF.Literal["No", "Yes"] + is_tax_withholding_account: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data 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 f511658067b..453f4f5ceab 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 @@ -865,6 +865,10 @@ class TestTaxWithholdingCategory(IntegrationTestCase): jv.apply_tds = 1 jv.tax_withholding_category = "Cumulative Threshold TDS" jv.save() + + # Again saving should not change tds amount + jv.user_remark = "Test TDS on Journal Entry for Supplier" + jv.save() jv.submit() # TDS = 50000 * 10% = 5000 @@ -884,7 +888,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # Supplier amount should be reduced by TDS self.assertEqual(supplier_row.credit, 45000) - jv.cancel() def test_tcs_on_journal_entry_for_customer(self): @@ -904,6 +907,10 @@ class TestTaxWithholdingCategory(IntegrationTestCase): jv.apply_tds = 1 jv.tax_withholding_category = "Cumulative Threshold TCS" jv.save() + + # Again saving should not change tds amount + jv.user_remark = "Test TCS on Journal Entry for Customer" + jv.save() jv.submit() # Assert TCS calculation (10% on amount above threshold of 30000) @@ -924,7 +931,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # Customer amount should be increased by TCS self.assertEqual(customer_row.debit, 52000) - jv.cancel()