Merge pull request #32646 from niralisatapara/tds_purchase_invoice

feat: Item Wise TDS Calculation
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index e73d602..370c0fc 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",
@@ -1422,6 +1423,26 @@
    "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",
    "fieldtype": "Section Break",
@@ -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 a8f6f80..5c1cb0d 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 7eddd81..30ed91b 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_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 @@
 	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_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 @@
 			{"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 @@
 	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 @@
 	)
 
 	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 @@
 		):
 			# 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 @@
 			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 @@
 	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 e80fe11..40c732b 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 @@
 		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 b5836c9..81de682 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -58,12 +58,25 @@
 		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 6a8c21f..2624181 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -317,3 +317,4 @@
 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 0000000..a333c5d
--- /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 677ca78..46ac808 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1207,7 +1207,7 @@
 			"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 @@
 		}
 
 		// 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",