From 705a26a2fa5d1dc20c07dd4150771529c707e820 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 9 Nov 2024 15:14:21 +0530 Subject: [PATCH] fix: better gls for purchases with tax witholding (#42743) * fix: better gls for purchases with tax witholding * test: test case for purchase invoice gl entries with tax witholding * fix: use flag `_skip_merge` instead of skipping merge based on against account * test: fix test `test_single_threshold_tds` for newer implementation (cherry picked from commit e3cd6539c34891fae6aab3012b8c13145ccc84ec) --- .../purchase_invoice/purchase_invoice.py | 81 +++++++++++++------ .../purchase_invoice/test_purchase_invoice.py | 55 +++++++++++++ .../test_tax_withholding_category.py | 10 ++- erpnext/accounts/general_ledger.py | 4 + 4 files changed, 123 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b46fb9e9b30b..b47e90eb77dd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -863,6 +863,7 @@ def get_gl_entries(self, warehouse_account=None): self.make_tax_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) + self.make_gl_entries_for_tax_withholding(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) @@ -896,32 +897,37 @@ def make_supplier_gl_entry(self, gl_entries): ) if grand_total and not self.is_internal_transfer(): - against_voucher = self.name - if self.is_return and self.return_against and not self.update_outstanding_for_self: - against_voucher = self.return_against + self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total) + + def add_supplier_gl_entry( + self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False + ): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + + # Did not use base_grand_total to book rounding loss gle + gl = { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "due_date": self.due_date, + "against": against_account or self.against_expense_account, + "credit": base_grand_total, + "credit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": against_voucher, + "against_voucher_type": self.doctype, + "project": self.project, + "cost_center": self.cost_center, + "_skip_merge": skip_merge, + } - # Did not use base_grand_total to book rounding loss gle - gl_entries.append( - self.get_gl_dict( - { - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "due_date": self.due_date, - "against": self.against_expense_account, - "credit": base_grand_total, - "credit_in_account_currency": base_grand_total - if self.party_account_currency == self.company_currency - else grand_total, - "against_voucher": against_voucher, - "against_voucher_type": self.doctype, - "project": self.project, - "cost_center": self.cost_center, - }, - self.party_account_currency, - item=self, - ) - ) + if remarks: + gl["remarks"] = remarks + + gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self)) def make_item_gl_entries(self, gl_entries): # item gl entries @@ -1413,6 +1419,31 @@ def make_internal_transfer_gl_entries(self, gl_entries): ) ) + def make_gl_entries_for_tax_withholding(self, gl_entries): + """ + Tax withholding amount is not part of supplier invoice. + Separate supplier GL Entry for correct reporting. + """ + if not self.apply_tds: + return + + for row in self.get("taxes"): + if not row.is_tax_withholding_account or not row.tax_amount: + continue + + base_tds_amount = row.base_tax_amount_after_discount_amount + tds_amount = row.tax_amount_after_discount_amount + + self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount) + self.add_supplier_gl_entry( + gl_entries, + -base_tds_amount, + -tds_amount, + against_account=row.account_head, + remarks=_("TDS Deducted"), + skip_merge=True, + ) + def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries if cint(self.is_paid) and self.cash_bank_account and self.paid_amount: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index cf5bfedaebdd..f5835deb0d01 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1544,6 +1544,61 @@ def test_purchase_invoice_advance_taxes(self): payment_entry.load_from_db() self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def test_purchase_gl_with_tax_withholding_tax(self): + company = "_Test Company" + + tds_account_args = { + "doctype": "Account", + "account_name": "TDS Payable", + "account_type": "Tax", + "parent_account": frappe.db.get_value( + "Account", {"account_name": "Duties and Taxes", "company": company} + ), + "company": company, + } + + tds_account = create_account(**tds_account_args) + tax_withholding_category = "Test TDS - 194 - Dividends - Individual" + + # Update tax withholding category with current fiscal year and rate details + create_tax_witholding_category(tax_withholding_category, company, tds_account) + + # create a new supplier to test + supplier = create_supplier( + supplier_name="_Test TDS Advance Supplier", + tax_withholding_category=tax_withholding_category, + ) + + pi = make_purchase_invoice( + supplier=supplier.name, + rate=3000, + qty=1, + item="_Test Non Stock Item", + do_not_submit=1, + ) + pi.apply_tds = 1 + pi.tax_withholding_category = tax_withholding_category + pi.save() + pi.submit() + + self.assertEqual(pi.taxes[0].tax_amount, 300) + self.assertEqual(pi.taxes[0].account_head, tds_account) + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"}, + fields=["account", "against", "debit", "credit"], + ) + + for gle in gl_entries: + if gle.debit: + # GL Entry with TDS Amount + self.assertEqual(gle.against, tds_account) + self.assertEqual(gle.debit, 300) + else: + # GL Entry with Purchase Invoice Amount + self.assertEqual(gle.credit, 3000) + def test_provisional_accounting_entry(self): setup_provisional_accounting() 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 c8893a13e866..2b7ae5fd6895 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 @@ -74,11 +74,17 @@ def test_single_threshold_tds(self): self.assertEqual(pi.grand_total, 18000) # check gl entry for the purchase invoice - gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"]) + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pi.name}, + fields=["account", "sum(debit) as debit", "sum(credit) as credit"], + group_by="account", + ) self.assertEqual(len(gl_entries), 3) for d in gl_entries: if d.account == pi.credit_to: - self.assertEqual(d.credit, 18000) + self.assertEqual(d.credit, 20000) + self.assertEqual(d.debit, 2000) elif d.account == pi.items[0].get("expense_account"): self.assertEqual(d.debit, 20000) elif d.account == pi.taxes[0].get("account_head"): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 856e2b96af09..f9b503675aaf 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -234,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None): merge_properties = get_merge_properties(accounting_dimensions) for entry in gl_map: + if entry._skip_merge: + merged_gl_map.append(entry) + continue + entry.merge_key = get_merge_key(entry, merge_properties) # if there is already an entry in this account then just add it # to that entry