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()