feat: Validity for Item taxes (#20135)

* feat: Validity for Item taxes

* fix: Trigger for gst hsn code

* fix: Sort taxes based on validity

* fix: Validation for item tax template and filters based on validity

* fix: Add missing semicolon

* fix: Validate tax template only if item code available

* fix: Do not validate or filter item tax template if no item taxes applied

* fix: Consider item group for validating taxes

* fix: Test cases for item tax  validation

* fix: Item tax template filtering fixes

* fix: Add missing semicolon

* fix: Remove unnecessary query
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 530bd89..a2a47b3 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -5,7 +5,7 @@
 import frappe
 
 import unittest, copy, time
-from frappe.utils import nowdate, flt, getdate, cint
+from frappe.utils import nowdate, flt, getdate, cint, add_days
 from frappe.model.dynamic_links import get_dynamic_link_map
 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
@@ -1847,6 +1847,26 @@
 		self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
 		self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
 
+	def test_item_tax_validity(self):
+		item = frappe.get_doc("Item", "_Test Item 2")
+
+		if item.taxes:
+			item.taxes = []
+			item.save()
+
+		item.append("taxes", {
+			"item_tax_template": "_Test Item Tax Template 1",
+			"valid_from": add_days(nowdate(), 1)
+		})
+
+		item.save()
+
+		sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
+		sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1"
+		self.assertRaises(frappe.ValidationError, sales_invoice.save)
+
+		item.taxes = []
+		item.save()
 
 def create_sales_invoice(**args):
 	si = frappe.new_doc("Sales Invoice")
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 5c31900..d18f8e5 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -4,9 +4,9 @@
 from __future__ import unicode_literals
 import frappe
 from frappe.desk.reportview import get_match_cond, get_filters_cond
-from frappe.utils import nowdate
+from frappe.utils import nowdate, getdate
 from collections import defaultdict
-
+from erpnext.stock.get_item_details import _get_item_tax_template
 
  # searches for active employees
 def employee_query(doctype, txt, searchfield, start, page_len, filters):
@@ -486,7 +486,7 @@
 @frappe.whitelist()
 def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
 	query = """
-		select pr.name 
+		select pr.name
 		from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem
 		where pr.docstatus = 1 and pritem.parent = pr.name
 		and pr.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt)))
@@ -499,7 +499,7 @@
 @frappe.whitelist()
 def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
 	query = """
-		select pi.name 
+		select pi.name
 		from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem
 		where pi.docstatus = 1 and piitem.parent = pi.name
 		and pi.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt)))
@@ -508,3 +508,27 @@
 		query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code')))
 
 	return frappe.db.sql(query, filters)
+
+@frappe.whitelist()
+def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
+
+	item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
+	item_group = filters.get('item_group')
+	taxes = item_doc.taxes or []
+
+	while item_group:
+		item_group_doc = frappe.get_cached_doc('Item Group', item_group)
+		taxes += item_group_doc.taxes or []
+		item_group = item_group_doc.parent_item_group
+
+	if not taxes:
+		return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
+	else:
+		args = {
+			'item_code': filters.get('item_code'),
+			'posting_date': filters.get('valid_from'),
+			'tax_category': filters.get('tax_category')
+		}
+
+		taxes = _get_item_tax_template(args, taxes, for_validate=True)
+		return [(d,) for d in set(taxes)]
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 049a837..b52a07d 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -8,6 +8,7 @@
 from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
 from erpnext.controllers.accounts_controller import validate_conversion_rate, \
 	validate_taxes_and_charges, validate_inclusive_tax
+from erpnext.stock.get_item_details import _get_item_tax_template
 
 class calculate_taxes_and_totals(object):
 	def __init__(self, doc):
@@ -34,6 +35,7 @@
 	def _calculate(self):
 		self.validate_conversion_rate()
 		self.calculate_item_values()
+		self.validate_item_tax_template()
 		self.initialize_taxes()
 		self.determine_exclusive_rate()
 		self.calculate_net_total()
@@ -43,6 +45,38 @@
 		self._cleanup()
 		self.calculate_total_net_weight()
 
+	def validate_item_tax_template(self):
+		for item in self.doc.get('items'):
+			if item.item_code and item.get('item_tax_template'):
+				item_doc = frappe.get_cached_doc("Item", item.item_code)
+				args = {
+					'tax_category': self.doc.get('tax_category'),
+					'posting_date': self.doc.get('posting_date'),
+					'bill_date': self.doc.get('bill_date'),
+					'transaction_date': self.doc.get('transaction_date')
+				}
+
+				item_group = item_doc.item_group
+				item_group_taxes = []
+
+				while item_group:
+					item_group_doc = frappe.get_cached_doc('Item Group', item_group)
+					item_group_taxes += item_group_doc.taxes or []
+					item_group = item_group_doc.parent_item_group
+
+				item_taxes = item_doc.taxes or []
+
+				if not item_group_taxes and (not item_taxes):
+					# No validation if no taxes in item or item group
+					continue
+
+				taxes = _get_item_tax_template(args, item_taxes + item_group_taxes, for_validate=True)
+
+				if item.item_tax_template not in taxes:
+					frappe.throw(_("Row {0}: Invalid Item Tax Template for item {1}").format(
+						item.idx, frappe.bold(item.item_code)
+					))
+
 	def validate_conversion_rate(self):
 		# validate conversion rate
 		company_currency = erpnext.get_company_currency(self.doc.company)
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 926227b..3d4c4a6 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -107,6 +107,12 @@
 				filters:{ 'item_code': row.item_code }
 			}
 		});
+
+		if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
+			this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
+				return me.set_query_for_item_tax_template(doc, cdt, cdn)
+			});
+		}
 	},
 
 	refresh: function(doc) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 748e623..51ab48a 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1700,6 +1700,29 @@
 		}
 	},
 
+	set_query_for_item_tax_template: function(doc, cdt, cdn) {
+
+		var item = frappe.get_doc(cdt, cdn);
+		if(!item.item_code) {
+			frappe.throw(__("Please enter Item Code to get item taxes"));
+		} else {
+
+			let filters = {
+				'item_code': item.item_code,
+				'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date,
+				'item_group': item.item_group,
+			}
+
+			if (doc.tax_category)
+				filters['tax_category'] = doc.tax_category;
+
+			return {
+				query: "erpnext.controllers.queries.get_tax_template",
+				filters: filters
+			}
+		}
+	},
+
 	payment_terms_template: function() {
 		var me = this;
 		const doc = this.frm.doc;
diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py
index fa2cb12..86cd4d1 100644
--- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py
+++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py
@@ -25,5 +25,9 @@
 		item_to_be_updated.taxes = []
 		for tax in taxes:
 			tax = frappe._dict(tax)
-			item_to_be_updated.append("taxes", {'item_tax_template': tax.item_tax_template, 'tax_category': tax.tax_category})
+			item_to_be_updated.append("taxes", {
+				'item_tax_template': tax.item_tax_template,
+				'tax_category': tax.tax_category,
+				'valid_from': tax.valid_from
+			})
 			item_to_be_updated.save()
\ No newline at end of file
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 1c9b30b..8278745 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -84,6 +84,13 @@
 				return me.set_query_for_batch(doc, cdt, cdn)
 			});
 		}
+
+		if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
+			this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
+				return me.set_query_for_item_tax_template(doc, cdt, cdn)
+			});
+		}
+
 	},
 
 	refresh: function() {
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index e3d356f..253390a 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -49,7 +49,7 @@
 		if (!frm.doc.is_fixed_asset) {
 			erpnext.item.make_dashboard(frm);
 		}
-		
+
 		if (frm.doc.is_fixed_asset) {
 			frm.trigger('is_fixed_asset');
 			frm.trigger('auto_create_assets');
@@ -136,14 +136,14 @@
 		frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0);
 	},
 
-	gst_hsn_code: function(frm){
-		if(!frm.doc.taxes){
-			frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc=>{
-				frm.doc.taxes = [];
+	gst_hsn_code: function(frm) {
+		if(!frm.doc.taxes.length) {
+			frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => {
 				$.each(hsn_doc.taxes || [], function(i, tax) {
 					let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes');
 					a.item_tax_template = tax.item_tax_template;
 					a.tax_category = tax.tax_category;
+					a.valid_from = tax.valid_from;
 					frm.refresh_field('taxes');
 				});
 			});
diff --git a/erpnext/stock/doctype/item_tax/item_tax.json b/erpnext/stock/doctype/item_tax/item_tax.json
index 37daa29..a93e463 100644
--- a/erpnext/stock/doctype/item_tax/item_tax.json
+++ b/erpnext/stock/doctype/item_tax/item_tax.json
@@ -1,107 +1,50 @@
 {
- "allow_copy": 0, 
- "allow_events_in_timeline": 0, 
- "allow_guest_to_view": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "beta": 0, 
- "creation": "2013-02-22 01:28:01", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "editable_grid": 1, 
+ "actions": [],
+ "creation": "2013-02-22 01:28:01",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_tax_template",
+  "tax_category",
+  "valid_from"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "item_tax_template", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Item Tax Template", 
-   "length": 0, 
-   "no_copy": 0, 
-   "oldfieldname": "tax_type", 
-   "oldfieldtype": "Link", 
-   "options": "Item Tax Template", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "item_tax_template",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Item Tax Template",
+   "oldfieldname": "tax_type",
+   "oldfieldtype": "Link",
+   "options": "Item Tax Template",
+   "reqd": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fetch_from": "", 
-   "fieldname": "tax_category", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Tax Category", 
-   "length": 0, 
-   "no_copy": 0, 
-   "oldfieldname": "tax_rate", 
-   "oldfieldtype": "Currency", 
-   "options": "Tax Category", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
+   "fieldname": "tax_category",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Tax Category",
+   "oldfieldname": "tax_rate",
+   "oldfieldtype": "Currency",
+   "options": "Tax Category"
+  },
+  {
+   "fieldname": "valid_from",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "label": "Valid From"
   }
- ], 
- "has_web_view": 0, 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 1, 
- "image_view": 0, 
- "in_create": 0, 
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 1, 
- "max_attachments": 0, 
- "modified": "2018-12-21 23:52:40.798944", 
- "modified_by": "Administrator", 
- "module": "Stock", 
- "name": "Item Tax", 
- "owner": "Administrator", 
- "permissions": [], 
- "quick_entry": 0, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "track_changes": 0, 
- "track_seen": 0, 
- "track_views": 0
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2019-12-28 21:54:40.807849",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Tax",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
 }
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 76644ed..4e5b933 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -4,7 +4,7 @@
 from __future__ import unicode_literals
 import frappe
 from frappe import _, throw
-from frappe.utils import flt, cint, add_days, cstr, add_months
+from frappe.utils import flt, cint, add_days, cstr, add_months, getdate
 import json, copy
 from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item, set_transaction_type
 from erpnext.setup.utils import get_exchange_rate
@@ -52,6 +52,16 @@
 
 	out = get_basic_details(args, item, overwrite_warehouse)
 
+	if isinstance(doc, string_types):
+		doc = json.loads(doc)
+
+	if doc and doc.get('doctype') == 'Purchase Invoice':
+		args['bill_date'] = doc.get('bill_date')
+
+	if doc:
+		args['posting_date'] = doc.get('posting_date')
+		args['transaction_date'] = doc.get('transaction_date')
+
 	get_item_tax_template(args, item, out)
 	out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \
 		else out.get("item_tax_template"), as_json=True)
@@ -395,7 +405,34 @@
 			item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
 			item_group = item_group_doc.parent_item_group
 
-def _get_item_tax_template(args, taxes, out):
+def _get_item_tax_template(args, taxes, out={}, for_validate=False):
+	taxes_with_validity = []
+	taxes_with_no_validity = []
+
+	for tax in taxes:
+		if tax.valid_from:
+			# In purchase Invoice first preference will be given to supplier invoice date
+			# if supplier date is not present then posting date
+			validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
+
+			if getdate(tax.valid_from) <= getdate(validation_date):
+				taxes_with_validity.append(tax)
+		else:
+			taxes_with_no_validity.append(tax)
+
+	if taxes_with_validity:
+		taxes = sorted(taxes_with_validity, key = lambda i: i.valid_from, reverse=True)
+	else:
+		taxes = taxes_with_no_validity
+
+	if for_validate:
+		return [tax.item_tax_template for tax in taxes if (cstr(tax.tax_category) == cstr(args.get('tax_category')) \
+			and (tax.item_tax_template not in taxes))]
+
+	# all templates have validity and no template is valid
+	if not taxes_with_validity and (not taxes_with_no_validity):
+		return None
+
 	for tax in taxes:
 		if cstr(tax.tax_category) == cstr(args.get("tax_category")):
 			out["item_tax_template"] = tax.item_tax_template