feat: split ledger entries for applications created across allocations

- fix: ledger entry for expiring cf leaves not considering holidays
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index f989945..4c09456 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -137,21 +137,36 @@
 	def validate_dates_across_allocation(self):
 		if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
 			return
-		def _get_leave_allocation_record(date):
-			allocation = frappe.db.sql("""select name from `tabLeave Allocation`
-				where employee=%s and leave_type=%s and docstatus=1
-				and %s between from_date and to_date""", (self.employee, self.leave_type, date))
 
-			return allocation and allocation[0][0]
+		alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+
+		if not (alloc_on_from_date or alloc_on_to_date):
+			frappe.throw(_("Application period cannot be outside leave allocation period"))
+
+		elif alloc_on_from_date.name != alloc_on_to_date.name:
+			frappe.throw(_("Application period cannot be across two allocation records"))
+
+	def get_allocation_based_on_application_dates(self):
+		"""Returns allocation name, from and to dates for application dates"""
+		def _get_leave_allocation_record(date):
+			LeaveAllocation = frappe.qb.DocType("Leave Allocation")
+			allocation = (
+				frappe.qb.from_(LeaveAllocation)
+				.select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date)
+				.where(
+					(LeaveAllocation.employee == self.employee)
+					& (LeaveAllocation.leave_type == self.leave_type)
+					& (LeaveAllocation.docstatus == 1)
+					& ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date))
+				)
+			).run(as_dict=True)
+
+			return allocation and allocation[0]
 
 		allocation_based_on_from_date = _get_leave_allocation_record(self.from_date)
 		allocation_based_on_to_date = _get_leave_allocation_record(self.to_date)
 
-		if not (allocation_based_on_from_date or allocation_based_on_to_date):
-			frappe.throw(_("Application period cannot be outside leave allocation period"))
-
-		elif allocation_based_on_from_date != allocation_based_on_to_date:
-			frappe.throw(_("Application period cannot be across two allocation records"))
+		return allocation_based_on_from_date, allocation_based_on_to_date
 
 	def validate_back_dated_application(self):
 		future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
@@ -433,49 +448,97 @@
 
 		expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type,
 			self.to_date, self.from_date)
-
 		lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
 
 		if expiry_date:
 			self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
 		else:
-			raise_exception = True
-			if frappe.flags.in_patch:
-				raise_exception=False
+			alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+			if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
+				# required only if negative balance is allowed for leave type
+				# else will be stopped in validation itself
+				self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp)
+			else:
+				raise_exception = False if frappe.flags.in_patch else True
+				args = dict(
+					leaves=self.total_leave_days * -1,
+					from_date=self.from_date,
+					to_date=self.to_date,
+					is_lwp=lwp,
+					holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
+				)
+				create_leave_ledger_entry(self, args, submit)
 
-			args = dict(
-				leaves=self.total_leave_days * -1,
+	def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool:
+		if ((alloc_on_from_date and not alloc_on_to_date)
+			or (not alloc_on_from_date and alloc_on_to_date)
+			or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)):
+			return True
+		return False
+
+	def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp):
+		"""Creates separate ledger entries for application period falling into separate allocations"""
+		# for creating separate ledger entries existing allocation periods should be consecutive
+		if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date:
+			frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format(
+				get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date)))
+
+		raise_exception = False if frappe.flags.in_patch else True
+		leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type,
+			self.from_date, alloc_on_from_date.to_date, self.half_day, self.half_day_date)
+		leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type,
+			add_days(alloc_on_from_date.to_date, 1), self.to_date, self.half_day, self.half_day_date)
+
+		args = dict(
+			is_lwp=lwp,
+			holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
+		)
+
+		if leaves_in_first_alloc:
+			args.update(dict(
 				from_date=self.from_date,
+				to_date=alloc_on_from_date.to_date,
+				leaves=leaves_in_first_alloc * -1
+			))
+			create_leave_ledger_entry(self, args, submit)
+
+		if leaves_in_second_alloc:
+			args.update(dict(
+				from_date=add_days(alloc_on_from_date.to_date, 1),
 				to_date=self.to_date,
+				leaves=leaves_in_second_alloc * -1
+			))
+			create_leave_ledger_entry(self, args, submit)
+
+	def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
+		"""Splits leave application into two ledger entries to consider expiry of allocation"""
+		raise_exception = False if frappe.flags.in_patch else True
+
+		leaves = get_number_of_leave_days(self.employee, self.leave_type,
+			self.from_date, expiry_date, self.half_day, self.half_day_date)
+
+		if leaves:
+			args = dict(
+				from_date=self.from_date,
+				to_date=expiry_date,
+				leaves=leaves * -1,
 				is_lwp=lwp,
 				holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
 			)
 			create_leave_ledger_entry(self, args, submit)
 
-	def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
-		''' splits leave application into two ledger entries to consider expiry of allocation '''
-
-		raise_exception = True
-		if frappe.flags.in_patch:
-			raise_exception=False
-
-		args = dict(
-			from_date=self.from_date,
-			to_date=expiry_date,
-			leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
-			is_lwp=lwp,
-			holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
-		)
-		create_leave_ledger_entry(self, args, submit)
-
 		if getdate(expiry_date) != getdate(self.to_date):
 			start_date = add_days(expiry_date, 1)
-			args.update(dict(
-				from_date=start_date,
-				to_date=self.to_date,
-				leaves=date_diff(self.to_date, expiry_date) * -1
-			))
-			create_leave_ledger_entry(self, args, submit)
+			leaves = get_number_of_leave_days(self.employee, self.leave_type,
+				start_date, self.to_date, self.half_day, self.half_day_date)
+
+			if leaves:
+				args.update(dict(
+					from_date=start_date,
+					to_date=self.to_date,
+					leaves=leaves * -1
+				))
+				create_leave_ledger_entry(self, args, submit)
 
 
 def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date):