Merge pull request #25961 from deepeshgarg007/dynamic_gst_rates

feat: Item Taxes based on net rate
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 5409a6f..5010fdc 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2001,6 +2001,33 @@
 		self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
 		self.assertTrue(einvoice['EwbDtls'])
 
+	def test_item_tax_net_range(self):
+		item = create_item("T Shirt")
+
+		item.set('taxes', [])
+		item.append("taxes", {
+			"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+			"minimum_net_rate": 0,
+			"maximum_net_rate": 500
+		})
+
+		item.append("taxes", {
+			"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+			"minimum_net_rate": 501,
+			"maximum_net_rate": 1000
+		})
+
+		item.save()
+
+		sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
+		self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
+
+		# Apply discount
+		sales_invoice.apply_discount_on = 'Net Total'
+		sales_invoice.discount_amount = 300
+		sales_invoice.save()
+		self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
+
 def make_test_address_for_ewaybill():
 	if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
 		address = frappe.get_doc({
@@ -2123,27 +2150,6 @@
 		doc.assertEqual(expected_gle[i][2], gle.credit)
 		doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
 
-	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 - _TC",
-			"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 - _TC"
-		self.assertRaises(frappe.ValidationError, sales_invoice.save)
-
-		item.taxes = []
-		item.save()
-
 def create_sales_invoice(**args):
 	si = frappe.new_doc("Sales Invoice")
 	args = frappe._dict(args)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 9fae494..fb22a1d 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -54,6 +54,7 @@
 			if item.item_code and item.get('item_tax_template'):
 				item_doc = frappe.get_cached_doc("Item", item.item_code)
 				args = {
+					'net_rate': item.net_rate or item.rate,
 					'tax_category': self.doc.get('tax_category'),
 					'posting_date': self.doc.get('posting_date'),
 					'bill_date': self.doc.get('bill_date'),
@@ -77,10 +78,12 @@
 
 				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)
-					))
+				if taxes:
+					if item.item_tax_template not in taxes:
+						item.item_tax_template = taxes[0]
+						frappe.msgprint(_("Row {0}: Item Tax template updated as per validity and rate applied").format(
+							item.idx, frappe.bold(item.item_code)
+						))
 
 	def validate_conversion_rate(self):
 		# validate conversion rate
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 2e133be..0b9d771 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -12,7 +12,7 @@
 		if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
 			effective_item_rate = item.blanket_order_rate;
 		}
-		if(item.margin_type == "Percentage"){
+		if (item.margin_type == "Percentage") {
 			item.rate_with_margin = flt(effective_item_rate)
 				+ flt(effective_item_rate) * ( flt(item.margin_rate_or_amount) / 100);
 		} else {
@@ -22,7 +22,7 @@
 
 		item_rate = flt(item.rate_with_margin , precision("rate", item));
 
-		if(item.discount_percentage){
+		if (item.discount_percentage) {
 			item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
 		}
 
@@ -73,15 +73,18 @@
 	},
 
 	_calculate_taxes_and_totals: function() {
-		this.validate_conversion_rate();
-		this.calculate_item_values();
-		this.initialize_taxes();
-		this.determine_exclusive_rate();
-		this.calculate_net_total();
-		this.calculate_taxes();
-		this.manipulate_grand_total_for_inclusive_tax();
-		this.calculate_totals();
-		this._cleanup();
+		frappe.run_serially([
+			() => this.validate_conversion_rate(),
+			() => this.calculate_item_values(),
+			() => this.update_item_tax_map(),
+			() => this.initialize_taxes(),
+			() => this.determine_exclusive_rate(),
+			() => this.calculate_net_total(),
+			() => this.calculate_taxes(),
+			() => this.manipulate_grand_total_for_inclusive_tax(),
+			() => this.calculate_totals(),
+			() => this._cleanup()
+		]);
 	},
 
 	validate_conversion_rate: function() {
@@ -263,6 +266,65 @@
 		frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
 	},
 
+	update_item_tax_map: function() {
+		let me = this;
+		let item_codes = [];
+		let item_rates = {};
+		$.each(this.frm.doc.items || [], function(i, item) {
+			if (item.item_code) {
+				// Use combination of name and item code in case same item is added multiple times
+				item_codes.push([item.item_code, item.name]);
+				item_rates[item.name] = item.net_rate;
+			}
+		});
+
+		if (item_codes.length) {
+			return this.frm.call({
+				method: "erpnext.stock.get_item_details.get_item_tax_info",
+				args: {
+					company: me.frm.doc.company,
+					tax_category: cstr(me.frm.doc.tax_category),
+					item_codes: item_codes,
+					item_rates: item_rates
+				},
+				callback: function(r) {
+					if (!r.exc) {
+						$.each(me.frm.doc.items || [], function(i, item) {
+							if (item.name && r.message.hasOwnProperty(item.name)) {
+								item.item_tax_template = r.message[item.name].item_tax_template;
+								item.item_tax_rate = r.message[item.name].item_tax_rate;
+								me.add_taxes_from_item_tax_template(item.item_tax_rate);
+							} else {
+								item.item_tax_template = "";
+								item.item_tax_rate = "{}";
+							}
+						});
+					}
+				}
+			});
+		}
+	},
+
+	add_taxes_from_item_tax_template: function(item_tax_map) {
+		let me = this;
+
+		if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
+			if (typeof (item_tax_map) == "string") {
+				item_tax_map = JSON.parse(item_tax_map);
+			}
+
+			$.each(item_tax_map, function(tax, rate) {
+				let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
+				if (!found) {
+					let child = frappe.model.add_child(me.frm.doc, "taxes");
+					child.charge_type = "On Net Total";
+					child.account_head = tax;
+					child.rate = 0;
+				}
+			});
+		}
+	},
+
 	calculate_taxes: function() {
 		var me = this;
 		this.frm.doc.rounding_adjustment = 0;
@@ -406,6 +468,11 @@
 		let tax_detail = tax.item_wise_tax_detail;
 		let key = item.item_code || item.item_name;
 
+		if(typeof (tax_detail) == "string") {
+			tax.item_wise_tax_detail = JSON.parse(tax.item_wise_tax_detail);
+			tax_detail = tax.item_wise_tax_detail;
+		}
+
 		let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate;
 		if (tax_detail && tax_detail[key])
 			item_wise_tax_amount += tax_detail[key][1];
@@ -562,6 +629,8 @@
 				tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
 			});
 		}
+
+		this.frm.refresh_fields();
 	},
 
 	set_discount_amount: function() {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 982b1fe..89fed3b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -6,6 +6,7 @@
 erpnext.TransactionController = erpnext.taxes_and_totals.extend({
 	setup: function() {
 		this._super();
+		let me = this;
 		frappe.flags.hide_serial_batch_dialog = true;
 		frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
 			var item = frappe.get_doc(cdt, cdn);
@@ -43,8 +44,6 @@
 			cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
 		});
 
-
-
 		frappe.ui.form.on(this.frm.cscript.tax_table, "rate", function(frm, cdt, cdn) {
 			cur_frm.cscript.calculate_taxes_and_totals();
 		});
@@ -121,7 +120,6 @@
 			}
 		});
 
-		var me = this;
 		if(this.frm.fields_dict["items"].grid.get_field('batch_no')) {
 			this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
 				return me.set_query_for_batch(doc, cdt, cdn);
@@ -564,6 +562,7 @@
 							name: me.frm.doc.name,
 							project: item.project || me.frm.doc.project,
 							qty: item.qty || 1,
+							net_rate: item.rate,
 							stock_qty: item.stock_qty,
 							conversion_factor: item.conversion_factor,
 							weight_per_unit: item.weight_per_unit,
@@ -720,26 +719,6 @@
 		});
 	},
 
-	add_taxes_from_item_tax_template: function(item_tax_map) {
-		let me = this;
-
-		if(item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
-			if(typeof (item_tax_map) == "string") {
-				item_tax_map = JSON.parse(item_tax_map);
-			}
-
-			$.each(item_tax_map, function(tax, rate) {
-				let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
-				if(!found) {
-					let child = frappe.model.add_child(me.frm.doc, "taxes");
-					child.charge_type = "On Net Total";
-					child.account_head = tax;
-					child.rate = 0;
-				}
-			});
-		}
-	},
-
 	serial_no: function(doc, cdt, cdn) {
 		var me = this;
 		var item = frappe.get_doc(cdt, cdn);
@@ -843,9 +822,9 @@
 
 				frappe.run_serially([
 					() => me.frm.script_manager.trigger("currency"),
-					() => me.update_item_tax_map(),
 					() => me.apply_default_taxes(),
-					() => me.apply_pricing_rule()
+					() => me.apply_pricing_rule(),
+					() => me.calculate_taxes_and_totals()
 				]);
 			}
 		}
@@ -1824,7 +1803,6 @@
 				callback: function(r) {
 					if(!r.exc) {
 						item.item_tax_rate = r.message;
-						me.add_taxes_from_item_tax_template(item.item_tax_rate);
 						me.calculate_taxes_and_totals();
 					}
 				}
@@ -1835,43 +1813,7 @@
 		}
 	},
 
-	update_item_tax_map: function() {
-		var me = this;
-		var item_codes = [];
-		$.each(this.frm.doc.items || [], function(i, item) {
-			if(item.item_code) {
-				item_codes.push(item.item_code);
-			}
-		});
 
-		if(item_codes.length) {
-			return this.frm.call({
-				method: "erpnext.stock.get_item_details.get_item_tax_info",
-				args: {
-					company: me.frm.doc.company,
-					tax_category: cstr(me.frm.doc.tax_category),
-					item_codes: item_codes
-				},
-				callback: function(r) {
-					if(!r.exc) {
-						$.each(me.frm.doc.items || [], function(i, item) {
-							if(item.item_code && r.message.hasOwnProperty(item.item_code)) {
-								if (!item.item_tax_template) {
-									item.item_tax_template = r.message[item.item_code].item_tax_template;
-									item.item_tax_rate = r.message[item.item_code].item_tax_rate;
-								}
-								me.add_taxes_from_item_tax_template(item.item_tax_rate);
-							} else {
-								item.item_tax_template = "";
-								item.item_tax_rate = "{}";
-							}
-						});
-						me.calculate_taxes_and_totals();
-					}
-				}
-			});
-		}
-	},
 
 	is_recurring: function() {
 		// set default values for recurring documents
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index dd81540..fbd30cf 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -122,6 +122,7 @@
 		self.validate_auto_reorder_enabled_in_stock_settings()
 		self.cant_change()
 		self.update_show_in_website()
+		self.validate_item_tax_net_rate_range()
 
 		if not self.is_new():
 			self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@@ -485,6 +486,11 @@
 		if self.disabled:
 			self.show_in_website = False
 
+	def validate_item_tax_net_rate_range(self):
+		for tax in self.get('taxes'):
+			if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
+				frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
+
 	def update_template_tables(self):
 		template = frappe.get_doc("Item", self.variant_of)
 
diff --git a/erpnext/stock/doctype/item_tax/item_tax.json b/erpnext/stock/doctype/item_tax/item_tax.json
index ae36efc..fb10096 100644
--- a/erpnext/stock/doctype/item_tax/item_tax.json
+++ b/erpnext/stock/doctype/item_tax/item_tax.json
@@ -1,4 +1,5 @@
 {
+ "actions": [],
  "creation": "2013-02-22 01:28:01",
  "doctype": "DocType",
  "editable_grid": 1,
@@ -6,7 +7,9 @@
  "field_order": [
   "item_tax_template",
   "tax_category",
-  "valid_from"
+  "valid_from",
+  "minimum_net_rate",
+  "maximum_net_rate"
  ],
  "fields": [
   {
@@ -33,11 +36,24 @@
    "fieldtype": "Date",
    "in_list_view": 1,
    "label": "Valid From"
+  },
+  {
+   "fieldname": "maximum_net_rate",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Maximum Net Rate"
+  },
+  {
+   "fieldname": "minimum_net_rate",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Minimum Net Rate"
   }
  ],
  "idx": 1,
  "istable": 1,
- "modified": "2020-06-25 01:40:28.859752",
+ "links": [],
+ "modified": "2021-06-03 13:20:06.982303",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item Tax",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index d1dcdc2..746cbbf 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -436,18 +436,22 @@
 	return itemwise_barcode
 
 @frappe.whitelist()
-def get_item_tax_info(company, tax_category, item_codes):
+def get_item_tax_info(company, tax_category, item_codes, item_rates=None):
 	out = {}
 	if isinstance(item_codes, string_types):
 		item_codes = json.loads(item_codes)
 
+	if isinstance(item_rates, string_types):
+		item_rates = json.loads(item_rates)
+
 	for item_code in item_codes:
-		if not item_code or item_code in out:
+		if not item_code or item_code[1] in out:
 			continue
-		out[item_code] = {}
-		item = frappe.get_cached_doc("Item", item_code)
-		get_item_tax_template({"company": company, "tax_category": tax_category}, item, out[item_code])
-		out[item_code]["item_tax_rate"] = get_item_tax_map(company, out[item_code].get("item_tax_template"), as_json=True)
+		out[item_code[1]] = {}
+		item = frappe.get_cached_doc("Item", item_code[0])
+		args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]}
+		get_item_tax_template(args, item, out[item_code[1]])
+		out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True)
 
 	return out
 
@@ -478,12 +482,13 @@
 
 	for tax in taxes:
 		tax_company = frappe.get_value("Item Tax Template", tax.item_tax_template, 'company')
-		if tax.valid_from and tax_company == args['company']:
+		if (tax.valid_from or tax.maximum_net_rate) and tax_company == args['company']:
 			# 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):
+			if getdate(tax.valid_from) <= getdate(validation_date) \
+				and is_within_valid_range(args, tax):
 				taxes_with_validity.append(tax)
 		else:
 			if tax_company == args['company']:
@@ -502,12 +507,25 @@
 	if not taxes_with_validity and (not taxes_with_no_validity):
 		return None
 
+	# do not change if already a valid template
+	if args.get('item_tax_template') in taxes:
+		return args.get('item_tax_template')
+
 	for tax in taxes:
 		if cstr(tax.tax_category) == cstr(args.get("tax_category")):
 			out["item_tax_template"] = tax.item_tax_template
 			return tax.item_tax_template
 	return None
 
+def is_within_valid_range(args, tax):
+	if not flt(tax.maximum_net_rate):
+		# No range specified, just ignore
+		return True
+	elif flt(tax.minimum_net_rate) <= flt(args.get('net_rate')) <= flt(tax.maximum_net_rate):
+		return True
+
+	return False
+
 @frappe.whitelist()
 def get_item_tax_map(company, item_tax_template, as_json=True):
 	item_tax_map = {}