Merge branch 'develop' into leave-opening-balance
diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
index c9239ed..aed901a 100644
--- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
@@ -2,6 +2,7 @@
 # License: GNU General Public License v3. See license.txt
 
 import unittest
+from contextlib import contextmanager
 from datetime import timedelta
 
 import frappe
@@ -30,3 +31,24 @@
 		"holidays" : holiday_dates
 		}).insert()
 	return doc
+
+
+@contextmanager
+def set_holiday_list(holiday_list, company_name):
+	"""
+	Context manager for setting holiday list in tests
+	"""
+	try:
+		company = frappe.get_doc('Company', company_name)
+		previous_holiday_list = company.default_holiday_list
+
+		company.default_holiday_list = holiday_list
+		company.save()
+
+		yield
+
+	finally:
+		# restore holiday list setup
+		company = frappe.get_doc('Company', company_name)
+		company.default_holiday_list = previous_holiday_list
+		company.save()
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 70250f5..dbb3db3 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -1,6 +1,7 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+from typing import Dict
 
 import frappe
 from frappe import _
@@ -34,6 +35,8 @@
 class OverlapError(frappe.ValidationError): pass
 class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
 class NotAnOptionalHoliday(frappe.ValidationError): pass
+class InsufficientLeaveBalanceError(frappe.ValidationError):
+	pass
 
 from frappe.model.document import Document
 
@@ -260,15 +263,18 @@
 				frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave."))
 
 			if not is_lwp(self.leave_type):
-				self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
-					consider_all_leaves_in_the_allocation_period=True)
-				if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
+				leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
+					consider_all_leaves_in_the_allocation_period=True, for_consumption=True)
+				self.leave_balance = leave_balance.get("leave_balance")
+				leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
+
+				if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
 					if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
-						frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
-							.format(self.leave_type))
+						frappe.msgprint(_("Insufficient leave balance for Leave Type {0}")
+							.format(frappe.bold(self.leave_type)), title=_("Warning"), indicator="orange")
 					else:
-						frappe.throw(_("There is not enough leave balance for Leave Type {0}")
-							.format(self.leave_type))
+						frappe.throw(_("Insufficient leave balance for Leave Type {0}")
+							.format(self.leave_type), InsufficientLeaveBalanceError, title=_("Insufficient Balance"))
 
 	def validate_leave_overlap(self):
 		if not self.name:
@@ -425,7 +431,7 @@
 		if self.status != 'Approved' and submit:
 			return
 
-		expiry_date = get_allocation_expiry(self.employee, self.leave_type,
+		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")
@@ -472,7 +478,7 @@
 			create_leave_ledger_entry(self, args, submit)
 
 
-def get_allocation_expiry(employee, leave_type, to_date, from_date):
+def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date):
 	''' Returns expiry of carry forward allocation in leave ledger entry '''
 	expiry =  frappe.get_all("Leave Ledger Entry",
 		filters={
@@ -480,7 +486,8 @@
 			'leave_type': leave_type,
 			'is_carry_forward': 1,
 			'transaction_type': 'Leave Allocation',
-			'to_date': ['between', (from_date, to_date)]
+			'to_date': ['between', (from_date, to_date)],
+			'docstatus': 1
 		},fields=['to_date'])
 	return expiry[0]['to_date'] if expiry else None
 
@@ -543,7 +550,8 @@
 	return ret
 
 @frappe.whitelist()
-def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False):
+def get_leave_balance_on(employee, leave_type, date, to_date=None,
+	consider_all_leaves_in_the_allocation_period=False, for_consumption=False):
 	'''
 		Returns leave balance till date
 		:param employee: employee name
@@ -551,6 +559,11 @@
 		:param date: date to check balance on
 		:param to_date: future date to check for allocation expiry
 		:param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
+		:param for_consumption: flag to check if leave balance is required for consumption or display
+			eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave
+			in this case leave_balance = 10 but leave_balance_for_consumption = 1
+			if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1}
+			else, returns leave_balance (in this case 10)
 	'''
 
 	if not to_date:
@@ -560,11 +573,17 @@
 	allocation = allocation_records.get(leave_type, frappe._dict())
 
 	end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
-	expiry = get_allocation_expiry(employee, leave_type, to_date, date)
+	cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
 
 	leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
 
-	return get_remaining_leaves(allocation, leaves_taken, date, expiry)
+	remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry)
+
+	if for_consumption:
+		return remaining_leaves
+	else:
+		return remaining_leaves.get('leave_balance')
+
 
 def get_leave_allocation_records(employee, date, leave_type=None):
 	''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
@@ -616,38 +635,47 @@
 		}, fields=['SUM(total_leave_days) as leaves'])[0]
 	return leaves['leaves'] if leaves['leaves'] else 0.0
 
-def get_remaining_leaves(allocation, leaves_taken, date, expiry):
-	''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
+def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]:
+	'''Returns a dict of leave_balance and leave_balance_for_consumption
+	leave_balance returns the available leave balance
+	leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry
+	'''
 	def _get_remaining_leaves(remaining_leaves, end_date):
-
+		''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
 		if remaining_leaves > 0:
 			remaining_days = date_diff(end_date, date) + 1
 			remaining_leaves = min(remaining_days, remaining_leaves)
 
 		return remaining_leaves
 
-	total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
+	leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
 
-	if expiry and allocation.unused_leaves:
-		remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
-		remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry)
+	# balance for carry forwarded leaves
+	if cf_expiry and allocation.unused_leaves:
+		cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
+		remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
 
-		total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
+		leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
+		leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
 
-	return _get_remaining_leaves(total_leaves, allocation.to_date)
+	remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
+	return {
+		'leave_balance': leave_balance,
+		'leave_balance_for_consumption': remaining_leaves
+	}
 
-def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
+def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True):
 	leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
 	leave_days = 0
 
 	for leave_entry in leave_entries:
 		inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
 
-		if  inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
+		if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
 			leave_days += leave_entry.leaves
 
 		elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \
-			and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)):
+			and not skip_expired_leaves:
 			leave_days += leave_entry.leaves
 
 		elif leave_entry.transaction_type == 'Leave Application':
@@ -669,11 +697,6 @@
 
 	return leave_days
 
-def skip_expiry_leaves(leave_entry, date):
-	''' Checks whether the expired leaves coincide with the to_date of leave balance check.
-		This allows backdated leave entry creation for non carry forwarded allocation '''
-	end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
-	return True if end_date == date and not leave_entry.is_carry_forward else False
 
 def get_leave_entries(employee, leave_type, from_date, to_date):
 	''' Returns leave entries between from_date and to_date. '''
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6d27f4a..22c7a8f 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -503,7 +503,7 @@
 			leave_type_name="_Test_CF_leave_expiry",
 			is_carry_forward=1,
 			expire_carry_forwarded_leaves_after_days=90)
-		leave_type.submit()
+		leave_type.insert()
 
 		create_carry_forwarded_allocation(employee, leave_type)
 
@@ -725,19 +725,22 @@
 			carry_forward=1)
 		leave_allocation.submit()
 
-def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
+def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
 	allocation = frappe.get_doc({
 		"doctype": "Leave Allocation",
 		"employee": employee or "_T-Employee-00001",
 		"leave_type": leave_type or "_Test Leave Type",
 		"from_date": from_date or "2013-01-01",
 		"to_date": to_date or "2019-12-31",
-		"new_leaves_allocated": 30
+		"new_leaves_allocated": leaves or 30,
+		"carry_forward": carry_forward
 	})
 
 	allocation.insert(ignore_permissions=True)
 	allocation.submit()
 
+	return allocation
+
 def get_employee():
 	return frappe.get_doc("Employee", "_T-Employee-00001")
 
@@ -782,9 +785,10 @@
 	allocate_leave.submit()
 
 
-def get_first_sunday(holiday_list):
-	month_start_date = get_first_day(nowdate())
-	month_end_date = get_last_day(nowdate())
+def get_first_sunday(holiday_list, for_date=None):
+	date = for_date or getdate()
+	month_start_date = get_first_day(date)
+	month_end_date = get_last_day(date)
 	first_sunday = frappe.db.sql("""
 		select holiday_date from `tabHoliday`
 		where parent = %s
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
index 5c5299e..a5923e0 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -171,7 +171,7 @@
 	''' Expires remaining leaves in the on carried forward allocation '''
 	from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
 	leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type,
-		allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True)
+		allocation.from_date, allocation.to_date, skip_expired_leaves=False)
 	leaves = flt(allocation.leaves) + flt(leaves_taken)
 
 	# allow expired leaves entry to be created
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index b375b18..3a5f2fe 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -6,8 +6,9 @@
 
 import frappe
 from frappe import _
-from frappe.utils import add_days
+from frappe.utils import add_days, date_diff, getdate
 
+from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
 from erpnext.hr.doctype.leave_application.leave_application import (
 	get_leave_balance_on,
 	get_leaves_for_period,
@@ -15,6 +16,8 @@
 
 
 def execute(filters=None):
+	filters = frappe._dict(filters or {})
+
 	if filters.to_date <= filters.from_date:
 		frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
 
@@ -46,27 +49,27 @@
 		'label': _('Opening Balance'),
 		'fieldtype': 'float',
 		'fieldname': 'opening_balance',
-		'width': 130,
+		'width': 150,
 	}, {
-		'label': _('Leave Allocated'),
+		'label': _('New Leave(s) Allocated'),
 		'fieldtype': 'float',
 		'fieldname': 'leaves_allocated',
-		'width': 130,
+		'width': 200,
 	}, {
-		'label': _('Leave Taken'),
+		'label': _('Leave(s) Taken'),
 		'fieldtype': 'float',
 		'fieldname': 'leaves_taken',
-		'width': 130,
+		'width': 150,
 	}, {
-		'label': _('Leave Expired'),
+		'label': _('Leave(s) Expired'),
 		'fieldtype': 'float',
 		'fieldname': 'leaves_expired',
-		'width': 130,
+		'width': 150,
 	}, {
 		'label': _('Closing Balance'),
 		'fieldtype': 'float',
 		'fieldname': 'closing_balance',
-		'width': 130,
+		'width': 150,
 	}]
 
 	return columns
@@ -102,19 +105,18 @@
 				or ("HR Manager" in frappe.get_roles(user)):
 				if len(active_employees) > 1:
 					row = frappe._dict()
-				row.employee = employee.name,
+				row.employee = employee.name
 				row.employee_name = employee.employee_name
 
 				leaves_taken = get_leaves_for_period(employee.name, leave_type,
 					filters.from_date, filters.to_date) * -1
 
-				new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type)
-
-
-				opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition
+				new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
+					filters.from_date, filters.to_date, employee.name, leave_type)
+				opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
 
 				row.leaves_allocated = new_allocation
-				row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
+				row.leaves_expired = expired_leaves
 				row.opening_balance = opening
 				row.leaves_taken = leaves_taken
 
@@ -125,6 +127,25 @@
 
 	return data
 
+
+def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves):
+	# allocation boundary condition
+	# opening balance is the closing leave balance 1 day before the filter start date
+	opening_balance_date = add_days(filters.from_date, -1)
+	allocation = get_previous_allocation(filters.from_date, leave_type, employee)
+
+	if allocation and allocation.get("to_date") and opening_balance_date and \
+		getdate(allocation.get("to_date")) == getdate(opening_balance_date):
+		# if opening balance date is same as the previous allocation's expiry
+		# then opening balance should only consider carry forwarded leaves
+		opening_balance = carry_forwarded_leaves
+	else:
+		# else directly get leave balance on the previous day
+		opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date)
+
+	return opening_balance
+
+
 def get_conditions(filters):
 	conditions={
 		'status': 'Active',
@@ -140,29 +161,26 @@
 
 	return conditions
 
-def get_department_leave_approver_map(department=None):
 
+def get_department_leave_approver_map(department=None):
 	# get current department and all its child
 	department_list = frappe.get_list('Department',
-						filters={
-							'disabled': 0
-						},
-						or_filters={
-							'name': department,
-							'parent_department': department
-						},
-						fields=['name'],
-						pluck='name'
-					)
+		filters={'disabled': 0},
+		or_filters={
+			'name': department,
+			'parent_department': department
+		},
+		pluck='name'
+	)
 	# retrieve approvers list from current department and from its subsequent child departments
 	approver_list = frappe.get_all('Department Approver',
-						filters={
-							'parentfield': 'leave_approvers',
-							'parent': ('in', department_list)
-						},
-						fields=['parent', 'approver'],
-						as_list=1
-					)
+		filters={
+			'parentfield': 'leave_approvers',
+			'parent': ('in', department_list)
+		},
+		fields=['parent', 'approver'],
+		as_list=True
+	)
 
 	approvers = {}
 
@@ -171,39 +189,59 @@
 
 	return approvers
 
+
 def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
-
-	from frappe.utils import getdate
-
 	new_allocation = 0
 	expired_leaves = 0
+	carry_forwarded_leaves = 0
 
-	records= frappe.db.sql("""
-		SELECT
-			employee, leave_type, from_date, to_date, leaves, transaction_name,
-			transaction_type, is_carry_forward, is_expired
-		FROM `tabLeave Ledger Entry`
-		WHERE employee=%(employee)s AND leave_type=%(leave_type)s
-			AND docstatus=1
-			AND transaction_type = 'Leave Allocation'
-			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)
+	records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
 
 	for record in records:
+		# new allocation records with `is_expired=1` are created when leave expires
+		# these new records should not be considered, else it leads to negative leave balance
+		if record.is_expired:
+			continue
+
 		if record.to_date < getdate(to_date):
+			# leave allocations ending before to_date, reduce leaves taken within that period
+			# since they are already used, they won't expire
 			expired_leaves += record.leaves
+			expired_leaves += get_leaves_for_period(employee, leave_type,
+					record.from_date, record.to_date)
 
 		if record.from_date >= getdate(from_date):
-			new_allocation += record.leaves
+			if record.is_carry_forward:
+				carry_forwarded_leaves += record.leaves
+			else:
+				new_allocation += record.leaves
 
-	return new_allocation, expired_leaves
+	return new_allocation, expired_leaves, carry_forwarded_leaves
+
+
+def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
+	ledger = frappe.qb.DocType('Leave Ledger Entry')
+	records = (
+		frappe.qb.from_(ledger)
+			.select(
+				ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date,
+				ledger.leaves, ledger.transaction_name, ledger.transaction_type,
+				ledger.is_carry_forward, ledger.is_expired
+			).where(
+				(ledger.docstatus == 1)
+				& (ledger.transaction_type == 'Leave Allocation')
+				& (ledger.employee == employee)
+				& (ledger.leave_type == leave_type)
+				& (
+					(ledger.from_date[from_date: to_date])
+					| (ledger.to_date[from_date: to_date])
+					| ((ledger.from_date < from_date) & (ledger.to_date > to_date))
+				)
+			)
+	).run(as_dict=True)
+
+	return records
+
 
 def get_chart_data(data):
 	labels = []
@@ -224,6 +262,7 @@
 
 	return chart
 
+
 def get_dataset_for_chart(employee_data, datasets, labels):
 	leaves = []
 	employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
new file mode 100644
index 0000000..05316f1
--- /dev/null
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -0,0 +1,162 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+import unittest
+
+import frappe
+from frappe.utils import (
+	add_days,
+	add_months,
+	get_year_ending,
+	get_year_start,
+	getdate,
+	flt,
+)
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday, make_allocation_record
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list, make_leave_application
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
+
+test_records = frappe.get_test_records('Leave Type')
+
+class TestEmployeeLeaveBalance(unittest.TestCase):
+	def setUp(self):
+		for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']:
+			frappe.db.delete(dt)
+
+		frappe.set_user('Administrator')
+
+		self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
+		self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', get_year_start(getdate()), get_year_ending(getdate()))
+
+		self.date = getdate()
+		self.year_start = getdate(get_year_start(self.date))
+		self.mid_year = add_months(self.year_start, 6)
+		self.year_end = getdate(get_year_ending(self.date))
+
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+	def test_employee_leave_balance(self):
+		frappe.get_doc(test_records[0]).insert()
+
+		# 5 leaves
+		allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11),
+			to_date=add_days(self.year_start, -1), leaves=5)
+		# 30 leaves
+		allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
+		# expires 5 leaves
+		process_expired_allocation()
+
+		# 4 days leave
+		first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+		leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type')
+		leave_application.reload()
+
+		filters = {
+			'from_date': allocation1.from_date,
+			'to_date': allocation2.to_date,
+			'employee': self.employee_id
+		}
+
+		report = execute(filters)
+
+		expected_data = [{
+			'leave_type': '_Test Leave Type',
+			'employee': self.employee_id,
+			'employee_name': 'test_emp_leave_balance@example.com',
+			'leaves_allocated': flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated),
+			'leaves_expired': flt(allocation1.new_leaves_allocated),
+			'opening_balance': flt(0),
+			'leaves_taken': flt(leave_application.total_leave_days),
+			'closing_balance': flt(allocation2.new_leaves_allocated - leave_application.total_leave_days),
+			'indent': 1
+		}]
+
+		self.assertEqual(report[1], expected_data)
+
+	@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+	def test_opening_balance_on_alloc_boundary_dates(self):
+		frappe.get_doc(test_records[0]).insert()
+
+		# 30 leaves allocated
+		allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
+		# 4 days leave application in the first allocation
+		first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+		leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type')
+		leave_application.reload()
+
+		# Case 1: opening balance for first alloc boundary
+		filters = {
+			'from_date': self.year_start,
+			'to_date': self.year_end,
+			'employee': self.employee_id
+		}
+		report = execute(filters)
+		self.assertEqual(report[1][0].opening_balance, 0)
+
+		# Case 2: opening balance after leave application date
+		filters = {
+			'from_date': add_days(leave_application.to_date, 1),
+			'to_date': self.year_end,
+			'employee': self.employee_id
+		}
+		report = execute(filters)
+		self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
+
+		# Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date
+		# eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
+		filters = {
+			'from_date': add_days(self.year_end, -3),
+			'to_date': self.year_end,
+			'employee': self.employee_id
+		}
+		report = execute(filters)
+		self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
+
+	@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
+	def test_opening_balance_considers_carry_forwarded_leaves(self):
+		leave_type = create_leave_type(
+			leave_type_name="_Test_CF_leave_expiry",
+			is_carry_forward=1)
+		leave_type.insert()
+
+		# 30 leaves allocated for first half of the year
+		allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start,
+			to_date=self.mid_year, leave_type=leave_type.name)
+		# 4 days leave application in the first allocation
+		first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+		leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name)
+		leave_application.reload()
+		# 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation
+		allocation2 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.mid_year, 1), to_date=self.year_end,
+			carry_forward=True, leave_type=leave_type.name)
+
+		# Case 1: carry forwarded leaves considered in opening balance for second alloc
+		filters = {
+			'from_date': add_days(self.mid_year, 1),
+			'to_date': self.year_end,
+			'employee': self.employee_id
+		}
+		report = execute(filters)
+		# available leaves from old alloc
+		opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days
+		self.assertEqual(report[1][0].opening_balance, opening_balance)
+
+		# Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc
+		filters = {
+			'from_date': add_days(self.mid_year, 2),
+			'to_date': self.year_end,
+			'employee': self.employee_id
+		}
+		report = execute(filters)
+		# available leaves from old alloc
+		opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days)
+		self.assertEqual(report[1][0].opening_balance, opening_balance)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 6a5debf..d34f6a6 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1019,15 +1019,16 @@
 	frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
 	frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
 
-def make_holiday_list(holiday_list_name=None):
+def make_holiday_list(list_name=None, from_date=None, to_date=None):
 	fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
-	holiday_list = frappe.db.exists("Holiday List", holiday_list_name or "Salary Slip Test Holiday List")
+	name = list_name or "Salary Slip Test Holiday List"
+	holiday_list = frappe.db.exists("Holiday List", name)
 	if not holiday_list:
 		holiday_list = frappe.get_doc({
 			"doctype": "Holiday List",
-			"holiday_list_name": holiday_list_name or "Salary Slip Test Holiday List",
-			"from_date": fiscal_year[1],
-			"to_date": fiscal_year[2],
+			"holiday_list_name": name,
+			"from_date": from_date or fiscal_year[1],
+			"to_date": to_date or fiscal_year[2],
 			"weekly_off": "Sunday"
 		}).insert()
 		holiday_list.get_weekly_off_dates()