From 2ca0cf6fc4216cca086d81290b930b07bd454f24 Mon Sep 17 00:00:00 2001 From: niralisatapara Date: Wed, 2 Nov 2022 12:19:51 +0530 Subject: [PATCH] feat: item wise tds calculation --- .../purchase_invoice/purchase_invoice.json | 22 +++- .../purchase_invoice/test_purchase_invoice.py | 109 ------------------ .../tax_withholding_category.py | 19 +-- .../test_tax_withholding_category.py | 42 ++++++- erpnext/controllers/taxes_and_totals.py | 8 +- erpnext/patches.txt | 1 + erpnext/patches/v14_0/update_tds_fields.py | 25 ++++ erpnext/public/js/controllers/transaction.js | 4 +- 8 files changed, 107 insertions(+), 123 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 25b128b893..2f9ee97aa9 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", @@ -1421,6 +1423,24 @@ "label": "Is Old Subcontracting Flow", "read_only": 1 }, + { + "default": "0", + "fieldname": "tax_withholding_net_total", + "fieldtype": "Currency", + "label": "Tax Withholding Net Total", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "base_tax_withholding_net_total", + "fieldtype": "Currency", + "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", @@ -1583,4 +1603,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 58e29f1a61..76ea95528c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1574,35 +1574,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertTrue(return_pi.docstatus == 1) - def test_without_tds(self): - make_purchase_invoice_tds() - - def test_total_tds(self): - supplier = create_supplier( - supplier_name="_Test TDS Advance Supplier", - tax_withholding_category="TDS - 194 - Dividends - Individual", - ) - pi = make_purchase_invoice_tds(supplier= "_Test TDS Advance Supplier",total_tds = 1) - - sum_tds = 0 - for item in pi.items: - sum_tds += item.net_amount - - self.assertEqual(pi.tax_withholding_net_total, sum_tds) - for tax in pi.taxes: - self.assertEqual(tax.tax_amount, pi.tax_withholding_net_total * 0.10) - - def test_partial_tds(self): - pi = make_purchase_invoice_tds(supplier= "_Test TDS Advance Supplier",partial_tds = 1) - - sum_tds = 0 - for item in pi.items: - if item.apply_tds: - sum_tds += item.net_amount - - self.assertEqual(pi.tax_withholding_net_total, sum_tds) - for tax in pi.taxes: - self.assertEqual(tax.tax_amount, pi.tax_withholding_net_total * 0.10) def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( @@ -1711,86 +1682,6 @@ def make_purchase_invoice(**args): pi.submit() return pi -def make_purchase_invoice_tds(**args): - pi = frappe.new_doc("Purchase Invoice") - args = frappe._dict(args) - pi.posting_date = args.posting_date or today() - if args.posting_time: - pi.posting_time = args.posting_time - if args.update_stock: - pi.update_stock = 1 - if args.is_paid: - pi.is_paid = 1 - - if args.cash_bank_account: - pi.cash_bank_account = args.cash_bank_account - - pi.company = args.company or "_Test Company" - pi.supplier = args.supplier or "_Test Supplier" - pi.currency = args.currency or "INR" - pi.conversion_rate = args.conversion_rate or 1 - pi.is_return = args.is_return - pi.return_against = args.return_against - pi.is_subcontracted = args.is_subcontracted or 0 - pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" - pi.cost_center = args.parent_cost_center - - if args.total_tds or args.partial_tds: - pi.apply_tds = 1 - - pi.extend( - "items", - [ - { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.received_qty or 0, - "rejected_qty": args.rejected_qty or 0, - "rate": args.rate or 5000, - "price_list_rate": args.price_list_rate or 5000, - "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", - "discount_account": args.discount_account or None, - "discount_amount": args.discount_amount or 0, - "conversion_factor": 1.0, - "serial_no": args.serial_no, - "stock_uom": args.uom or "_Test UOM", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "project": args.project, - "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "", - "asset_location": args.location or "", - "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, - "apply_tds": 1 if (args.total_tds or args.partial_tds) else 0 - }, - { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.received_qty or 0, - "rejected_qty": args.rejected_qty or 0, - "rate": args.rate or 5000, - "price_list_rate": args.price_list_rate or 5000, - "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", - "discount_account": args.discount_account or None, - "discount_amount": args.discount_amount or 0, - "conversion_factor": 1.0, - "serial_no": args.serial_no, - "stock_uom": args.uom or "_Test UOM", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "project": args.project, - "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "", - "asset_location": args.location or "", - "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, - "apply_tds": 1 if (args.total_tds) else 0 - }, - ] - ) - - pi.save() - pi.submit() - return pi def make_purchase_invoice_against_cost_center(**args): pi = frappe.new_doc("Purchase Invoice") 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 737338e04f..06e1c17854 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -275,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 = [] @@ -291,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) @@ -431,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( @@ -559,4 +564,4 @@ def is_valid_certificate( ) and certificate_limit > deducted_amount: valid = True - return valid + return valid \ No newline at end of file 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 e80fe11ab3..d29af920de 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" @@ -559,4 +599,4 @@ def create_tax_with_holding_category(): ], "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], } - ).insert() + ).insert() \ No newline at end of file diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index a0bc6ba629..16bc01dc87 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -67,13 +67,15 @@ class calculate_taxes_and_totals(object): 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"): @@ -1076,4 +1078,4 @@ class init_landed_taxes_and_totals(object): def set_amounts_in_company_currency(self): for d in self.doc.get(self.tax_field): d.amount = flt(d.amount, d.precision("amount")) - d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) + d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6a8c21f654..2624181c19 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -317,3 +317,4 @@ erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization +erpnext.patches.v14_0.update_tds_fields 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 0000000000..ffada07495 --- /dev/null +++ b/erpnext/patches/v14_0/update_tds_fields.py @@ -0,0 +1,25 @@ +import frappe + +from erpnext.accounts.utils import 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"): + fiscal_year_details = get_fiscal_year(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() \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index dd957c72ac..c2e34a6422 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1200,7 +1200,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); @@ -1217,7 +1217,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",