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