feat: employee leave balance reports (#20754)

* feat: Employee leave balance summary report new design

* feat: Employee leave balance report new design

* fix: leave based on multiple holiday list

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index c441751..47b1bb7 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -374,7 +374,8 @@
 				leaves=self.total_leave_days * -1,
 				from_date=self.from_date,
 				to_date=self.to_date,
-				is_lwp=lwp
+				is_lwp=lwp,
+				holiday_list=get_holiday_list_for_employee(self.employee)
 			)
 			create_leave_ledger_entry(self, args, submit)
 
@@ -384,7 +385,9 @@
 			from_date=self.from_date,
 			to_date=expiry_date,
 			leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
-			is_lwp=lwp
+			is_lwp=lwp,
+			holiday_list=get_holiday_list_for_employee(self.employee),
+
 		)
 		create_leave_ledger_entry(self, args, submit)
 
@@ -410,7 +413,7 @@
 	return expiry[0]['to_date'] if expiry else None
 
 @frappe.whitelist()
-def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None):
+def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None):
 	number_of_days = 0
 	if cint(half_day) == 1:
 		if from_date == to_date:
@@ -424,7 +427,7 @@
 		number_of_days = date_diff(to_date, from_date) + 1
 
 	if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"):
-		number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date))
+		number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list))
 	return number_of_days
 
 @frappe.whitelist()
@@ -575,7 +578,7 @@
 					{'name': leave_entry.transaction_name}, ['half_day_date'])
 
 			leave_days += get_number_of_leave_days(employee, leave_type,
-				leave_entry.from_date, leave_entry.to_date, half_day, half_day_date) * -1
+				leave_entry.from_date, leave_entry.to_date, half_day, half_day_date, holiday_list=leave_entry.holiday_list) * -1
 
 	return leave_days
 
@@ -589,7 +592,7 @@
 	''' Returns leave entries between from_date and to_date. '''
 	return frappe.db.sql("""
 		SELECT
-			employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type,
+			employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list,
 			is_carry_forward, is_expired
 		FROM `tabLeave Ledger Entry`
 		WHERE employee=%(employee)s AND leave_type=%(leave_type)s
@@ -607,9 +610,10 @@
 	}, as_dict=1)
 
 @frappe.whitelist()
-def get_holidays(employee, from_date, to_date):
+def get_holidays(employee, from_date, to_date, holiday_list = None):
 	'''get holidays between two dates for the given employee'''
-	holiday_list = get_holiday_list_for_employee(employee)
+	if not holiday_list:
+		holiday_list = get_holiday_list_for_employee(employee)
 
 	holidays = frappe.db.sql("""select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2
 		where h1.parent = h2.name and h1.holiday_date between %s and %s
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
index 771e706..a5ac3f3 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
@@ -1,4 +1,5 @@
 {
+ "actions": [],
  "creation": "2019-05-09 15:47:39.760406",
  "doctype": "DocType",
  "engine": "InnoDB",
@@ -12,6 +13,7 @@
   "column_break_7",
   "from_date",
   "to_date",
+  "holiday_list",
   "is_carry_forward",
   "is_expired",
   "is_lwp",
@@ -98,11 +100,18 @@
    "fieldname": "is_lwp",
    "fieldtype": "Check",
    "label": "Is Leave Without Pay"
+  },
+  {
+   "fieldname": "holiday_list",
+   "fieldtype": "Link",
+   "label": "Holiday List",
+   "options": "Holiday List"
   }
  ],
  "in_create": 1,
  "is_submittable": 1,
- "modified": "2019-08-20 14:40:04.130799",
+ "links": [],
+ "modified": "2020-02-27 14:40:10.502605",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Leave Ledger Entry",
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 35c8630..97be5cd 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -1,85 +1,186 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
 
 from __future__ import unicode_literals
 import frappe
-from frappe import _
 from frappe.utils import flt
-from erpnext.hr.doctype.leave_application.leave_application \
-	import get_leave_balance_on, get_leaves_for_period
-
-from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary \
-	import get_department_leave_approver_map
+from frappe import _
+from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on, get_leave_allocation_records
 
 def execute(filters=None):
-	leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc")
+	if filters.to_date <= filters.from_date:
+		frappe.throw(_('From date can not be greater than than To date'))
 
-	columns = get_columns(leave_types)
-	data = get_data(filters, leave_types)
+	columns = get_columns()
+	data = get_data(filters)
 
 	return columns, data
 
-def get_columns(leave_types):
-	columns = [
-		_("Employee") + ":Link.Employee:150",
-		_("Employee Name") + "::200",
-		_("Department") +"::150"
-	]
-
-	for leave_type in leave_types:
-		columns.append(_(leave_type) + " " + _("Opening") + ":Float:160")
-		columns.append(_(leave_type) + " " + _("Taken") + ":Float:160")
-		columns.append(_(leave_type) + " " + _("Balance") + ":Float:160")
+def get_columns():
+	columns = [{
+		'label': _('Leave Type'),
+		'fieldtype': 'Link',
+		'fieldname': 'leave_type',
+		'width': 200,
+		'options': 'Leave Type'
+	}, {
+		'label': _('Employee'),
+		'fieldtype': 'Link',
+		'fieldname': 'employee',
+		'width': 100,
+		'options': 'Employee'
+	}, {
+		'label': _('Employee Name'),
+		'fieldtype': 'Data',
+		'fieldname': 'employee_name',
+		'width': 100,
+	}, {
+		'label': _('Opening Balance'),
+		'fieldtype': 'float',
+		'fieldname': 'opening_balance',
+		'width': 130,
+	}, {
+		'label': _('Leaves Allocated'),
+		'fieldtype': 'float',
+		'fieldname': 'leaves_allocated',
+		'width': 130,
+	}, {
+		'label': _('Leaves Taken'),
+		'fieldtype': 'float',
+		'fieldname': 'leaves_taken',
+		'width': 130,
+	}, {
+		'label': _('Leaves Expired'),
+		'fieldtype': 'float',
+		'fieldname': 'leaves_expired',
+		'width': 130,
+	}, {
+		'label': _('Closing Balance'),
+		'fieldtype': 'float',
+		'fieldname': 'closing_balance',
+		'width': 130,
+	}]
 
 	return columns
 
-def get_conditions(filters):
-	conditions = {
-		"status": "Active",
-		"company": filters.company,
-	}
-	if filters.get("department"):
-		conditions.update({"department": filters.get("department")})
-	if filters.get("employee"):
-		conditions.update({"employee": filters.get("employee")})
+def get_data(filters):
+	leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC")
 
-	return conditions
-
-def get_data(filters, leave_types):
-	user = frappe.session.user
 	conditions = get_conditions(filters)
 
-	if filters.to_date <= filters.from_date:
-		frappe.throw(_("From date can not be greater than than To date"))
-
-	active_employees = frappe.get_all("Employee",
-		filters=conditions,
-		fields=["name", "employee_name", "department", "user_id", "leave_approver"])
-
+	user = frappe.session.user
 	department_approver_map = get_department_leave_approver_map(filters.get('department'))
 
+	active_employees = frappe.get_list('Employee',
+		filters=conditions,
+		fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver'])
+
 	data = []
-	for employee in active_employees:
-		leave_approvers = department_approver_map.get(employee.department_name, [])
-		if employee.leave_approver:
-			leave_approvers.append(employee.leave_approver)
 
-		if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)):
-			row = [employee.name, employee.employee_name, employee.department]
+	for leave_type in leave_types:
+		if len(active_employees) > 1:
+			data.append({
+				'leave_type': leave_type
+			})
+		else:
+			row = frappe._dict({
+				'leave_type': leave_type
+			})
 
-			for leave_type in leave_types:
-				# leaves taken
+		for employee in active_employees:
+
+			leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver)
+
+			if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \
+				or ("HR Manager" in frappe.get_roles(user)):
+				if len(active_employees) > 1:
+					row = frappe._dict()
+				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
 
-				# opening balance
+				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, filters.from_date)
+				closing = get_leave_balance_on(employee.name, leave_type, filters.to_date)
 
-				# closing balance
-				closing = max(opening - leaves_taken, 0)
+				row.leaves_allocated = new_allocation
+				row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
+				row.opening_balance = opening
+				row.leaves_taken = leaves_taken
+				row.closing_balance = closing
+				row.indent = 1
+				data.append(row)
+				new_leaves_allocated = 0
 
-				row += [opening, leaves_taken, closing]
 
-			data.append(row)
+	return data
 
-	return data
\ No newline at end of file
+def get_conditions(filters):
+	conditions={
+		'status': 'Active',
+	}
+	if filters.get('employee'):
+		conditions['name'] = filters.get('employee')
+
+	if filters.get('employee'):
+		conditions['name'] = filters.get('employee')
+
+	return conditions
+
+def get_department_leave_approver_map(department=None):
+	conditions=''
+	if department:
+		conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
+
+	# get current department and all its child
+	department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
+
+	# 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)
+
+	approvers = {}
+
+	for k, v in approver_list:
+		approvers.setdefault(k, []).append(v)
+
+	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
+
+	records= frappe.db.sql("""
+		SELECT
+			employee, leave_type, from_date, to_date, leaves, transaction_name,
+			is_carry_forward, is_expired
+		FROM `tabLeave Ledger Entry`
+		WHERE employee=%(employee)s AND leave_type=%(leave_type)s
+			AND docstatus=1 AND leaves>0
+			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)
+
+	for record in records:
+		if record.to_date <= getdate(to_date):
+			expired_leaves += record.leaves
+
+		if record.from_date >= getdate(from_date):
+			new_allocation += record.leaves
+
+	return new_allocation, expired_leaves
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js
index 3fb8f6e..cb05d11 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js
@@ -5,18 +5,11 @@
 frappe.query_reports['Employee Leave Balance Summary'] = {
 	filters: [
 		{
-			fieldname:'from_date',
-			label: __('From Date'),
+			fieldname:'date',
+			label: __('Date'),
 			fieldtype: 'Date',
 			reqd: 1,
-			default: frappe.defaults.get_default('year_start_date')
-		},
-		{
-			fieldname:'to_date',
-			label: __('To Date'),
-			fieldtype: 'Date',
-			reqd: 1,
-			default: frappe.defaults.get_default('year_end_date')
+			default: frappe.datetime.now_date()
 		},
 		{
 			fieldname:'company',
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
index 777de02..a5cdecf 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
@@ -1,130 +1,75 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
 
 from __future__ import unicode_literals
 import frappe
-from frappe.utils import flt
 from frappe import _
-from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on
+from frappe.utils import flt
+from erpnext.hr.doctype.leave_application.leave_application \
+	import get_leave_balance_on, get_leaves_for_period
+
+from erpnext.hr.report.employee_leave_balance.employee_leave_balance \
+	import get_department_leave_approver_map
 
 def execute(filters=None):
-	if filters.to_date <= filters.from_date:
-		frappe.throw(_('From date can not be greater than than To date'))
+	leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc")
 
-	columns = get_columns()
-	data = get_data(filters)
+	columns = get_columns(leave_types)
+	data = get_data(filters, leave_types)
 
 	return columns, data
 
-def get_columns():
-	columns = [{
-		'label': _('Leave Type'),
-		'fieldtype': 'Link',
-		'fieldname': 'leave_type',
-		'width': 300,
-		'options': 'Leave Type'
-	}, {
-		'label': _('Employee'),
-		'fieldtype': 'Link',
-		'fieldname': 'employee',
-		'width': 100,
-		'options': 'Employee'
-	}, {
-		'label': _('Employee Name'),
-		'fieldtype': 'Data',
-		'fieldname': 'employee_name',
-		'width': 100,
-	}, {
-		'label': _('Opening Balance'),
-		'fieldtype': 'float',
-		'fieldname': 'opening_balance',
-		'width': 160,
-	}, {
-		'label': _('Leaves Taken'),
-		'fieldtype': 'float',
-		'fieldname': 'leaves_taken',
-		'width': 160,
-	}, {
-		'label': _('Closing Balance'),
-		'fieldtype': 'float',
-		'fieldname': 'closing_balance',
-		'width': 160,
-	}]
+def get_columns(leave_types):
+	columns = [
+		_("Employee") + ":Link.Employee:150",
+		_("Employee Name") + "::200",
+		_("Department") +"::150"
+	]
+
+	for leave_type in leave_types:
+		columns.append(_(leave_type) + ":Float:160")
 
 	return columns
 
-def get_data(filters):
-	leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC")
-
-	conditions = get_conditions(filters)
-
-	user = frappe.session.user
-	department_approver_map = get_department_leave_approver_map(filters.get('department'))
-
-	active_employees = frappe.get_list('Employee',
-		filters=conditions,
-		fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver'])
-
-	data = []
-
-	for leave_type in leave_types:
-		data.append({
-			'leave_type': leave_type
-		})
-		for employee in active_employees:
-			
-			leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver)
-
-			if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \
-				or ("HR Manager" in frappe.get_roles(user)):
-				row = frappe._dict({
-					'employee': employee.name,
-					'employee_name': employee.employee_name
-				})
-
-				leaves_taken = get_leaves_for_period(employee.name, leave_type,
-					filters.from_date, filters.to_date) * -1
-
-				opening = get_leave_balance_on(employee.name, leave_type, filters.from_date)
-				closing = get_leave_balance_on(employee.name, leave_type, filters.to_date)
-
-				row.opening_balance = opening
-				row.leaves_taken = leaves_taken
-				row.closing_balance = closing
-				row.indent = 1
-				data.append(row)
-
-	return data
-
 def get_conditions(filters):
-	conditions={
-		'status': 'Active',
+	conditions = {
+		"status": "Active",
+		"company": filters.company,
 	}
-	if filters.get('employee'):
-		conditions['name'] = filters.get('employee')
-
-	if filters.get('employee'):
-		conditions['name'] = filters.get('employee')
+	if filters.get("department"):
+		conditions.update({"department": filters.get("department")})
+	if filters.get("employee"):
+		conditions.update({"employee": filters.get("employee")})
 
 	return conditions
 
-def get_department_leave_approver_map(department=None):
-	conditions=''
-	if department:
-		conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
+def get_data(filters, leave_types):
+	user = frappe.session.user
+	conditions = get_conditions(filters)
 
-	# get current department and all its child
-	department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
+	active_employees = frappe.get_all("Employee",
+		filters=conditions,
+		fields=["name", "employee_name", "department", "user_id", "leave_approver"])
 
-	# 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)
+	department_approver_map = get_department_leave_approver_map(filters.get('department'))
 
-	approvers = {}
+	data = []
+	for employee in active_employees:
+		leave_approvers = department_approver_map.get(employee.department_name, [])
+		if employee.leave_approver:
+			leave_approvers.append(employee.leave_approver)
 
-	for k, v in approver_list:
-		approvers.setdefault(k, []).append(v)
+		if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)):
+			row = [employee.name, employee.employee_name, employee.department]
 
-	return approvers
+			for leave_type in leave_types:
+
+				# opening balance
+				opening = get_leave_balance_on(employee.name, leave_type, filters.date)
+
+
+				row += [opening]
+
+			data.append(row)
+
+	return data
\ No newline at end of file