Merge pull request #30573 from frappe/mergify/bp/develop/pr-30382

fix: Added validation for single_threshold in Tax With Holding Category (backport #30382)
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
index e658049..605ce83 100644
--- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -1,7 +1,8 @@
 {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
-{%- set einvoice = json.loads(doc.signed_einvoice) -%}
 
 <div class="page-break">
+	{% if doc.signed_einvoice %}
+	{%- set einvoice = json.loads(doc.signed_einvoice) -%}
 	<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
 		{% if letter_head and not no_letterhead %}
 			<div class="letter-head">{{ letter_head }}</div>
@@ -170,4 +171,10 @@
 			</tbody>
 		</table>
 	</div>
+	{% else %}
+	<div class="text-center" style="color: var(--gray-500); font-size: 14px;">
+		You must generate IRN before you can preview GST E-Invoice.
+	</div>
+	{% endif %}
 </div>
+
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
index d5fb969..e6f08f7 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
@@ -47,7 +47,7 @@
   "item_search_settings_section",
   "redisearch_warning",
   "search_index_fields",
-  "show_categories_in_search_autocomplete",
+  "is_redisearch_enabled",
   "is_redisearch_loaded",
   "shop_by_category_section",
   "slideshow",
@@ -293,6 +293,7 @@
    "fieldname": "search_index_fields",
    "fieldtype": "Small Text",
    "label": "Search Index Fields",
+   "mandatory_depends_on": "is_redisearch_enabled",
    "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
   },
   {
@@ -302,13 +303,6 @@
    "label": "Item Search Settings"
   },
   {
-   "default": "1",
-   "fieldname": "show_categories_in_search_autocomplete",
-   "fieldtype": "Check",
-   "label": "Show Categories in Search Autocomplete",
-   "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
-  },
-  {
    "default": "0",
    "fieldname": "is_redisearch_loaded",
    "fieldtype": "Check",
@@ -365,12 +359,19 @@
    "fieldname": "show_price_in_quotation",
    "fieldtype": "Check",
    "label": "Show Price in Quotation"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_redisearch_enabled",
+   "fieldtype": "Check",
+   "label": "Enable Redisearch",
+   "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
   }
  ],
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-09-02 14:02:44.785824",
+ "modified": "2022-04-01 18:35:56.106756",
  "modified_by": "Administrator",
  "module": "E-commerce",
  "name": "E Commerce Settings",
@@ -389,5 +390,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
index 881d833..f85667e 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
@@ -9,6 +9,7 @@
 
 from erpnext.e_commerce.redisearch_utils import (
 	create_website_items_index,
+	define_autocomplete_dictionary,
 	get_indexable_web_fields,
 	is_search_module_loaded,
 )
@@ -21,6 +22,8 @@
 class ECommerceSettings(Document):
 	def onload(self):
 		self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
+
+		# flag >> if redisearch is installed and loaded
 		self.is_redisearch_loaded = is_search_module_loaded()
 
 	def validate(self):
@@ -34,6 +37,20 @@
 
 		frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
 
+		self.is_redisearch_enabled_pre_save = frappe.db.get_single_value(
+			"E Commerce Settings", "is_redisearch_enabled"
+		)
+
+	def after_save(self):
+		self.create_redisearch_indexes()
+
+	def create_redisearch_indexes(self):
+		# if redisearch is enabled (value changed) create indexes and dictionary
+		value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save
+		if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed:
+			define_autocomplete_dictionary()
+			create_website_items_index()
+
 	def validate_field_filters(self):
 		if not (self.enable_field_filters and self.filter_fields):
 			return
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py
index 82829bf..f2dd796 100644
--- a/erpnext/e_commerce/redisearch_utils.py
+++ b/erpnext/e_commerce/redisearch_utils.py
@@ -1,8 +1,12 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+import json
+
 import frappe
+from frappe import _
 from frappe.utils.redis_wrapper import RedisWrapper
+from redis import ResponseError
 from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
 
 WEBSITE_ITEM_INDEX = "website_items_index"
@@ -22,6 +26,12 @@
 	return [df.fieldname for df in valid_fields]
 
 
+def is_redisearch_enabled():
+	"Return True only if redisearch is loaded and enabled."
+	is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled")
+	return is_search_module_loaded() and is_redisearch_enabled
+
+
 def is_search_module_loaded():
 	try:
 		cache = frappe.cache()
@@ -32,14 +42,14 @@
 		)
 		return "search" in parsed_output
 	except Exception:
-		return False
+		return False  # handling older redis versions
 
 
-def if_redisearch_loaded(function):
-	"Decorator to check if Redisearch is loaded."
+def if_redisearch_enabled(function):
+	"Decorator to check if Redisearch is enabled."
 
 	def wrapper(*args, **kwargs):
-		if is_search_module_loaded():
+		if is_redisearch_enabled():
 			func = function(*args, **kwargs)
 			return func
 		return
@@ -51,22 +61,25 @@
 	return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def create_website_items_index():
 	"Creates Index Definition."
 
 	# CREATE index
 	client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
 
-	# DROP if already exists
 	try:
-		client.drop_index()
-	except Exception:
+		client.drop_index()  # drop if already exists
+	except ResponseError:
+		# will most likely raise a ResponseError if index does not exist
+		# ignore and create index
 		pass
+	except Exception:
+		raise_redisearch_error()
 
 	idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
 
-	# Based on e-commerce settings
+	# Index fields mentioned in e-commerce settings
 	idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
 	idx_fields = idx_fields.split(",") if idx_fields else []
 
@@ -91,20 +104,20 @@
 	return TextField(field)
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def insert_item_to_index(website_item_doc):
 	# Insert item to index
 	key = get_cache_key(website_item_doc.name)
 	cache = frappe.cache()
 	web_item = create_web_item_map(website_item_doc)
 
-	for k, v in web_item.items():
-		super(RedisWrapper, cache).hset(make_key(key), k, v)
+	for field, value in web_item.items():
+		super(RedisWrapper, cache).hset(make_key(key), field, value)
 
 	insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def insert_to_name_ac(web_name, doc_name):
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
 	ac.add_suggestions(Suggestion(web_name, payload=doc_name))
@@ -114,20 +127,20 @@
 	fields_to_index = get_fields_indexed()
 	web_item = {}
 
-	for f in fields_to_index:
-		web_item[f] = website_item_doc.get(f) or ""
+	for field in fields_to_index:
+		web_item[field] = website_item_doc.get(field) or ""
 
 	return web_item
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def update_index_for_item(website_item_doc):
 	# Reinsert to Cache
 	insert_item_to_index(website_item_doc)
 	define_autocomplete_dictionary()
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def delete_item_from_index(website_item_doc):
 	cache = frappe.cache()
 	key = get_cache_key(website_item_doc.name)
@@ -135,13 +148,13 @@
 	try:
 		cache.delete(key)
 	except Exception:
-		return False
+		raise_redisearch_error()
 
 	delete_from_ac_dict(website_item_doc)
 	return True
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def delete_from_ac_dict(website_item_doc):
 	"""Removes this items's name from autocomplete dictionary"""
 	cache = frappe.cache()
@@ -149,40 +162,60 @@
 	name_ac.delete(website_item_doc.web_item_name)
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def define_autocomplete_dictionary():
-	"""Creates an autocomplete search dictionary for `name`.
-	Also creats autocomplete dictionary for `categories` if
-	checked in E Commerce Settings"""
+	"""
+	Defines/Redefines an autocomplete search dictionary for Website Item Name.
+	Also creats autocomplete dictionary for Published Item Groups.
+	"""
 
 	cache = frappe.cache()
-	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
-	cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
-
-	ac_categories = frappe.db.get_single_value(
-		"E Commerce Settings", "show_categories_in_search_autocomplete"
-	)
+	item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+	item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
 
 	# Delete both autocomplete dicts
 	try:
 		cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
 		cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
 	except Exception:
-		return False
+		raise_redisearch_error()
 
+	create_items_autocomplete_dict(autocompleter=item_ac)
+	create_item_groups_autocomplete_dict(autocompleter=item_group_ac)
+
+
+@if_redisearch_enabled
+def create_items_autocomplete_dict(autocompleter):
+	"Add items as suggestions in Autocompleter."
 	items = frappe.get_all(
 		"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
 	)
 
 	for item in items:
-		name_ac.add_suggestions(Suggestion(item.web_item_name))
-		if ac_categories and item.item_group:
-			cat_ac.add_suggestions(Suggestion(item.item_group))
-
-	return True
+		autocompleter.add_suggestions(Suggestion(item.web_item_name))
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
+def create_item_groups_autocomplete_dict(autocompleter):
+	"Add item groups with weightage as suggestions in Autocompleter."
+	published_item_groups = frappe.get_all(
+		"Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
+	)
+	if not published_item_groups:
+		return
+
+	for item_group in published_item_groups:
+		payload = json.dumps({"name": item_group.name, "route": item_group.route})
+		autocompleter.add_suggestions(
+			Suggestion(
+				string=item_group.name,
+				score=frappe.utils.flt(item_group.weightage) or 1.0,
+				payload=payload,  # additional info that can be retrieved later
+			)
+		)
+
+
+@if_redisearch_enabled
 def reindex_all_web_items():
 	items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
 
@@ -191,8 +224,8 @@
 		web_item = create_web_item_map(item)
 		key = make_key(get_cache_key(item.name))
 
-		for k, v in web_item.items():
-			super(RedisWrapper, cache).hset(key, k, v)
+		for field, value in web_item.items():
+			super(RedisWrapper, cache).hset(key, field, value)
 
 
 def get_cache_key(name):
@@ -210,7 +243,12 @@
 	return fields_to_index
 
 
-# TODO: Remove later
-# # Figure out a way to run this at startup
-define_autocomplete_dictionary()
-create_website_items_index()
+def raise_redisearch_error():
+	"Create an Error Log and raise error."
+	traceback = frappe.get_traceback()
+	log = frappe.log_error(traceback, frappe._("Redisearch Error"))
+	log_link = frappe.utils.get_link_to_form("Error Log", log.name)
+
+	frappe.throw(
+		msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error")
+	)
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 98408af..27479a5 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -39,11 +39,15 @@
 	def validate(self):
 		self.validate_period()
 		self.validate_allocation_overlap()
-		self.validate_back_dated_allocation()
-		self.set_total_leaves_allocated()
-		self.validate_total_leaves_allocated()
 		self.validate_lwp()
 		set_employee_name(self)
+		self.set_total_leaves_allocated()
+		self.validate_leave_days_and_dates()
+
+	def validate_leave_days_and_dates(self):
+		# all validations that should run on save as well as on update after submit
+		self.validate_back_dated_allocation()
+		self.validate_total_leaves_allocated()
 		self.validate_leave_allocation_days()
 
 	def validate_leave_allocation_days(self):
@@ -56,14 +60,19 @@
 			leave_allocated = 0
 			if leave_period:
 				leave_allocated = get_leave_allocation_for_period(
-					self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date
+					self.employee,
+					self.leave_type,
+					leave_period[0].from_date,
+					leave_period[0].to_date,
+					exclude_allocation=self.name,
 				)
 			leave_allocated += flt(self.new_leaves_allocated)
 			if leave_allocated > max_leaves_allowed:
 				frappe.throw(
 					_(
-						"Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period"
-					).format(self.leave_type, self.employee)
+						"Total allocated leaves are more than maximum allocation allowed for {0} leave type for employee {1} in the period"
+					).format(self.leave_type, self.employee),
+					OverAllocationError,
 				)
 
 	def on_submit(self):
@@ -84,6 +93,12 @@
 	def on_update_after_submit(self):
 		if self.has_value_changed("new_leaves_allocated"):
 			self.validate_against_leave_applications()
+
+			# recalculate total leaves allocated
+			self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
+			# run required validations again since total leaves are being updated
+			self.validate_leave_days_and_dates()
+
 			leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
 			args = {
 				"leaves": leaves_to_be_added,
@@ -92,6 +107,7 @@
 				"is_carry_forward": 0,
 			}
 			create_leave_ledger_entry(self, args, True)
+			self.db_update()
 
 	def get_existing_leave_count(self):
 		ledger_entries = frappe.get_all(
@@ -279,27 +295,27 @@
 	)
 
 
-def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
-	leave_allocated = 0
-	leave_allocations = frappe.db.sql(
-		"""
-		select employee, leave_type, from_date, to_date, total_leaves_allocated
-		from `tabLeave Allocation`
-		where employee=%(employee)s and leave_type=%(leave_type)s
-			and docstatus=1
-			and (from_date between %(from_date)s and %(to_date)s
-				or to_date between %(from_date)s and %(to_date)s
-				or (from_date < %(from_date)s and to_date > %(to_date)s))
-	""",
-		{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
-		as_dict=1,
-	)
+def get_leave_allocation_for_period(
+	employee, leave_type, from_date, to_date, exclude_allocation=None
+):
+	from frappe.query_builder.functions import Sum
 
-	if leave_allocations:
-		for leave_alloc in leave_allocations:
-			leave_allocated += leave_alloc.total_leaves_allocated
-
-	return leave_allocated
+	Allocation = frappe.qb.DocType("Leave Allocation")
+	return (
+		frappe.qb.from_(Allocation)
+		.select(Sum(Allocation.total_leaves_allocated).as_("total_allocated_leaves"))
+		.where(
+			(Allocation.employee == employee)
+			& (Allocation.leave_type == leave_type)
+			& (Allocation.docstatus == 1)
+			& (Allocation.name != exclude_allocation)
+			& (
+				(Allocation.from_date.between(from_date, to_date))
+				| (Allocation.to_date.between(from_date, to_date))
+				| ((Allocation.from_date < from_date) & (Allocation.to_date > to_date))
+			)
+		)
+	).run()[0][0] or 0.0
 
 
 @frappe.whitelist()
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index a53d4a8..dde52d7 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -1,24 +1,26 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, add_months, getdate, nowdate
 
 import erpnext
 from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_allocation.leave_allocation import (
+	BackDatedAllocationError,
+	OverAllocationError,
+)
 from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
 from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
 
 
-class TestLeaveAllocation(unittest.TestCase):
-	@classmethod
-	def setUpClass(cls):
-		frappe.db.sql("delete from `tabLeave Period`")
+class TestLeaveAllocation(FrappeTestCase):
+	def setUp(self):
+		frappe.db.delete("Leave Period")
+		frappe.db.delete("Leave Allocation")
 
-		emp_id = make_employee("test_emp_leave_allocation@salary.com")
-		cls.employee = frappe.get_doc("Employee", emp_id)
-
-	def tearDown(self):
-		frappe.db.rollback()
+		emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
+		self.employee = frappe.get_doc("Employee", emp_id)
 
 	def test_overlapping_allocation(self):
 		leaves = [
@@ -65,7 +67,7 @@
 		# invalid period
 		self.assertRaises(frappe.ValidationError, doc.save)
 
-	def test_allocated_leave_days_over_period(self):
+	def test_validation_for_over_allocation(self):
 		doc = frappe.get_doc(
 			{
 				"doctype": "Leave Allocation",
@@ -80,7 +82,135 @@
 		)
 
 		# allocated leave more than period
-		self.assertRaises(frappe.ValidationError, doc.save)
+		self.assertRaises(OverAllocationError, doc.save)
+
+	def test_validation_for_over_allocation_post_submission(self):
+		allocation = frappe.get_doc(
+			{
+				"doctype": "Leave Allocation",
+				"__islocal": 1,
+				"employee": self.employee.name,
+				"employee_name": self.employee.employee_name,
+				"leave_type": "_Test Leave Type",
+				"from_date": getdate("2015-09-1"),
+				"to_date": getdate("2015-09-30"),
+				"new_leaves_allocated": 15,
+			}
+		).submit()
+		allocation.reload()
+		# allocated leaves more than period after submission
+		allocation.new_leaves_allocated = 35
+		self.assertRaises(OverAllocationError, allocation.save)
+
+	def test_validation_for_over_allocation_based_on_leave_setup(self):
+		frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
+		leave_period = frappe.get_doc(
+			dict(
+				name="Test Allocation Period",
+				doctype="Leave Period",
+				from_date=add_months(nowdate(), -6),
+				to_date=add_months(nowdate(), 6),
+				company="_Test Company",
+				is_active=1,
+			)
+		).insert()
+
+		leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
+		leave_type.max_leaves_allowed = 25
+		leave_type.save()
+
+		# 15 leaves allocated in this period
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=leave_period.from_date,
+			to_date=nowdate(),
+		)
+		allocation.submit()
+
+		# trying to allocate additional 15 leaves
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=add_days(nowdate(), 1),
+			to_date=leave_period.to_date,
+		)
+		self.assertRaises(OverAllocationError, allocation.save)
+
+	def test_validation_for_over_allocation_based_on_leave_setup_post_submission(self):
+		frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
+		leave_period = frappe.get_doc(
+			dict(
+				name="Test Allocation Period",
+				doctype="Leave Period",
+				from_date=add_months(nowdate(), -6),
+				to_date=add_months(nowdate(), 6),
+				company="_Test Company",
+				is_active=1,
+			)
+		).insert()
+
+		leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
+		leave_type.max_leaves_allowed = 30
+		leave_type.save()
+
+		# 15 leaves allocated
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=leave_period.from_date,
+			to_date=nowdate(),
+		)
+		allocation.submit()
+		allocation.reload()
+
+		# allocate additional 15 leaves
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=add_days(nowdate(), 1),
+			to_date=leave_period.to_date,
+		)
+		allocation.submit()
+		allocation.reload()
+
+		# trying to allocate 25 leaves in 2nd alloc within leave period
+		# total leaves = 40 which is more than `max_leaves_allowed` setting i.e. 30
+		allocation.new_leaves_allocated = 25
+		self.assertRaises(OverAllocationError, allocation.save)
+
+	def test_validate_back_dated_allocation_update(self):
+		leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
+		leave_type.save()
+
+		# initial leave allocation = 15
+		leave_allocation = create_leave_allocation(
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			leave_type="_Test_CF_leave",
+			from_date=add_months(nowdate(), -12),
+			to_date=add_months(nowdate(), -1),
+			carry_forward=0,
+		)
+		leave_allocation.submit()
+
+		# new_leaves = 15, carry_forwarded = 10
+		leave_allocation_1 = create_leave_allocation(
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			leave_type="_Test_CF_leave",
+			carry_forward=1,
+		)
+		leave_allocation_1.submit()
+
+		# try updating initial leave allocation
+		leave_allocation.reload()
+		leave_allocation.new_leaves_allocated = 20
+		self.assertRaises(BackDatedAllocationError, leave_allocation.save)
 
 	def test_carry_forward_calculation(self):
 		leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
@@ -108,8 +238,10 @@
 			carry_forward=1,
 		)
 		leave_allocation_1.submit()
+		leave_allocation_1.reload()
 
 		self.assertEqual(leave_allocation_1.unused_leaves, 10)
+		self.assertEqual(leave_allocation_1.total_leaves_allocated, 25)
 
 		leave_allocation_1.cancel()
 
@@ -197,9 +329,12 @@
 			employee=self.employee.name, employee_name=self.employee.employee_name
 		)
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+
 		leave_allocation.new_leaves_allocated = 40
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 40)
 
 	def test_leave_subtraction_after_submit(self):
@@ -207,9 +342,12 @@
 			employee=self.employee.name, employee_name=self.employee.employee_name
 		)
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+
 		leave_allocation.new_leaves_allocated = 10
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 10)
 
 	def test_validation_against_leave_application_after_submit(self):
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 348f0c6..17b018c 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -105,6 +105,30 @@
 						},
 						primary_action_label: __('Submit')
 					});
+					d.fields_dict.transporter.df.onchange = function () {
+						const transporter = d.fields_dict.transporter.value;
+						if (transporter) {
+							frappe.db.get_value('Supplier', transporter, ['gst_transporter_id', 'supplier_name'])
+								.then(({ message }) => {
+									d.set_value('gst_transporter_id', message.gst_transporter_id);
+									d.set_value('transporter_name', message.supplier_name);
+								});
+						} else {
+							d.set_value('gst_transporter_id', '');
+							d.set_value('transporter_name', '');
+						}
+					};
+					d.fields_dict.driver.df.onchange = function () {
+						const driver = d.fields_dict.driver.value;
+						if (driver) {
+							frappe.db.get_value('Driver', driver, ['full_name'])
+								.then(({ message }) => {
+									d.set_value('driver_name', message.full_name);
+								});
+						} else {
+							d.set_value('driver_name', '');
+						}
+					};
 					d.show();
 				};
 
@@ -153,7 +177,6 @@
 			'fieldname': 'gst_transporter_id',
 			'label': 'GST Transporter ID',
 			'fieldtype': 'Data',
-			'fetch_from': 'transporter.gst_transporter_id',
 			'default': frm.doc.gst_transporter_id
 		},
 		{
@@ -189,9 +212,9 @@
 			'fieldname': 'transporter_name',
 			'label': 'Transporter Name',
 			'fieldtype': 'Data',
-			'fetch_from': 'transporter.name',
 			'read_only': 1,
-			'default': frm.doc.transporter_name
+			'default': frm.doc.transporter_name,
+			'depends_on': 'transporter'
 		},
 		{
 			'fieldname': 'mode_of_transport',
@@ -206,7 +229,8 @@
 			'fieldtype': 'Data',
 			'fetch_from': 'driver.full_name',
 			'read_only': 1,
-			'default': frm.doc.driver_name
+			'default': frm.doc.driver_name,
+			'depends_on': 'driver'
 		},
 		{
 			'fieldname': 'lr_date',
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index cbdec56..8fd9c1c 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -387,7 +387,7 @@
 
 def get_payment_details(invoice):
 	payee_name = invoice.company
-	mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments])
+	mode_of_payment = ""
 	paid_amount = invoice.base_paid_amount
 	outstanding_amount = invoice.outstanding_amount
 
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py
index 77a749e..3ed056f 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -1,6 +1,8 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+import json
+
 import frappe
 from frappe.utils import cint, cstr
 from redisearch import AutoCompleter, Client, Query
@@ -9,7 +11,7 @@
 	WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
 	WEBSITE_ITEM_INDEX,
 	WEBSITE_ITEM_NAME_AUTOCOMPLETE,
-	is_search_module_loaded,
+	is_redisearch_enabled,
 	make_key,
 )
 from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
@@ -74,8 +76,8 @@
 def product_search(query, limit=10, fuzzy_search=True):
 	search_results = {"from_redisearch": True, "results": []}
 
-	if not is_search_module_loaded():
-		# Redisearch module not loaded
+	if not is_redisearch_enabled():
+		# Redisearch module not enabled
 		search_results["from_redisearch"] = False
 		search_results["results"] = get_product_data(query, 0, limit)
 		return search_results
@@ -86,6 +88,8 @@
 	red = frappe.cache()
 	query = clean_up_query(query)
 
+	# TODO: Check perf/correctness with Suggestions & Query vs only Query
+	# TODO: Use Levenshtein Distance in Query (max=3)
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red)
 	client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red)
 	suggestions = ac.get_suggestions(
@@ -121,8 +125,8 @@
 def get_category_suggestions(query):
 	search_results = {"results": []}
 
-	if not is_search_module_loaded():
-		# Redisearch module not loaded, query db
+	if not is_redisearch_enabled():
+		# Redisearch module not enabled, query db
 		categories = frappe.db.get_all(
 			"Item Group",
 			filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1},
@@ -135,8 +139,10 @@
 		return search_results
 
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache())
-	suggestions = ac.get_suggestions(query, num=10)
+	suggestions = ac.get_suggestions(query, num=10, with_payloads=True)
 
-	search_results["results"] = [s.string for s in suggestions]
+	results = [json.loads(s.payload) for s in suggestions]
+
+	search_results["results"] = results
 
 	return search_results