mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 07:02:54 +00:00
Earned Leave (#14143)
* Earned Leave Allocations will be initially zero, escaped validation in leave allocation to allow this * Earned Leave monthly scheduler method, test * remove whitelist of method
This commit is contained in:
committed by
Nabin Hait
parent
f9f35d0b76
commit
375db6181d
@@ -238,7 +238,8 @@ scheduler_events = {
|
|||||||
"erpnext.assets.doctype.asset.asset.make_post_gl_entry"
|
"erpnext.assets.doctype.asset.asset.make_post_gl_entry"
|
||||||
],
|
],
|
||||||
"monthly": [
|
"monthly": [
|
||||||
"erpnext.accounts.doctype.sales_invoice.sales_invoice.booked_deferred_revenue"
|
"erpnext.accounts.doctype.sales_invoice.sales_invoice.booked_deferred_revenue",
|
||||||
|
"erpnext.hr.utils.allocate_earned_leaves"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ class LeaveAllocation(Document):
|
|||||||
|
|
||||||
self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated)
|
self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated)
|
||||||
|
|
||||||
if not self.total_leaves_allocated:
|
if not self.total_leaves_allocated and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave"):
|
||||||
frappe.throw(_("Total leaves allocated is mandatory"))
|
frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}".format(self.leave_type)))
|
||||||
|
|
||||||
def validate_total_leaves_allocated(self):
|
def validate_total_leaves_allocated(self):
|
||||||
# Adding a day to include To Date in the difference
|
# Adding a day to include To Date in the difference
|
||||||
@@ -158,4 +158,3 @@ def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
|
|||||||
def validate_carry_forward(leave_type):
|
def validate_carry_forward(leave_type):
|
||||||
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
|
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
|
||||||
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
|
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import unittest
|
|||||||
|
|
||||||
from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on
|
from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on
|
||||||
from frappe.permissions import clear_user_permissions_for_doctype
|
from frappe.permissions import clear_user_permissions_for_doctype
|
||||||
from frappe.utils import add_days, nowdate, now_datetime
|
from frappe.utils import add_days, nowdate, now_datetime, get_datetime
|
||||||
|
|
||||||
test_dependencies = ["Leave Allocation", "Leave Block List"]
|
test_dependencies = ["Leave Allocation", "Leave Block List"]
|
||||||
|
|
||||||
@@ -387,25 +387,32 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, leave_application.insert)
|
self.assertRaises(frappe.ValidationError, leave_application.insert)
|
||||||
|
|
||||||
# def test_earned_leave(self):
|
def test_earned_leave(self):
|
||||||
# leave_period = get_leave_period()
|
leave_period = get_leave_period()
|
||||||
# employee = get_employee()
|
employee = get_employee()
|
||||||
#
|
|
||||||
# leave_type = frappe.get_doc(dict(
|
|
||||||
# leave_type_name = 'Test Earned Leave Type',
|
|
||||||
# doctype = 'Leave Type',
|
|
||||||
# is_earned_leave = 1,
|
|
||||||
# earned_leave_frequency = 'Monthly',
|
|
||||||
# rounding = 0.5
|
|
||||||
# )).insert()
|
|
||||||
#
|
|
||||||
# allocate_leaves(employee, leave_period, leave_type.name, 0, eligible_leaves = 12)
|
|
||||||
#
|
|
||||||
# # this method will be called by scheduler
|
|
||||||
# allocate_earned_leaves(leave_type.name, leave_period, as_on = half_of_leave_period)
|
|
||||||
#
|
|
||||||
# self.assertEqual(get_leave_balance(employee, leave_period, leave_type.name), 6)
|
|
||||||
|
|
||||||
|
leave_type = frappe.get_doc(dict(
|
||||||
|
leave_type_name = 'Test Earned Leave Type',
|
||||||
|
doctype = 'Leave Type',
|
||||||
|
is_earned_leave = 1,
|
||||||
|
earned_leave_frequency = 'Monthly',
|
||||||
|
rounding = 0.5,
|
||||||
|
max_leaves_allowed = 6
|
||||||
|
)).insert()
|
||||||
|
leave_policy = frappe.get_doc({
|
||||||
|
"doctype": "Leave Policy",
|
||||||
|
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
|
||||||
|
}).insert()
|
||||||
|
frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name)
|
||||||
|
|
||||||
|
allocate_leaves(employee, leave_period, leave_type.name, 0, eligible_leaves = 12)
|
||||||
|
|
||||||
|
from erpnext.hr.utils import allocate_earned_leaves
|
||||||
|
i = 0
|
||||||
|
while(i<14):
|
||||||
|
allocate_earned_leaves()
|
||||||
|
i += 1
|
||||||
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate()), 6)
|
||||||
|
|
||||||
def make_allocation_record(employee=None, leave_type=None):
|
def make_allocation_record(employee=None, leave_type=None):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ class LeavePeriod(Document):
|
|||||||
allocation.leave_type = leave_type
|
allocation.leave_type = leave_type
|
||||||
allocation.from_date = self.from_date
|
allocation.from_date = self.from_date
|
||||||
allocation.to_date = self.to_date
|
allocation.to_date = self.to_date
|
||||||
allocation.new_leaves_allocated = new_leaves_allocated
|
'''Earned Leaves are allocated by scheduler, initially allocate 0'''
|
||||||
|
allocation.new_leaves_allocated = new_leaves_allocated if not frappe.db.get_value("Leave Type", leave_type, "is_earned_leave") else 0
|
||||||
allocation.leave_period = self.name
|
allocation.leave_period = self.name
|
||||||
if self.carry_forward_leaves:
|
if self.carry_forward_leaves:
|
||||||
if frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
|
if frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
|
||||||
|
|||||||
@@ -584,7 +584,7 @@
|
|||||||
"default": "0.5",
|
"default": "0.5",
|
||||||
"depends_on": "is_earned_leave",
|
"depends_on": "is_earned_leave",
|
||||||
"fieldname": "rounding",
|
"fieldname": "rounding",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
@@ -595,6 +595,7 @@
|
|||||||
"label": "Rounding",
|
"label": "Rounding",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
|
"options": "0.5\n1.0",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"precision": "",
|
"precision": "",
|
||||||
"print_hide": 0,
|
"print_hide": 0,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate
|
from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.desk.form import assign_to
|
from frappe.desk.form import assign_to
|
||||||
|
|
||||||
@@ -241,3 +241,52 @@ def get_payroll_period(from_date, to_date, company):
|
|||||||
pd.parent=pp.name where pd.start_date<=%s and pd.end_date>= %s
|
pd.parent=pp.name where pd.start_date<=%s and pd.end_date>= %s
|
||||||
and pp.company=%s""", (from_date, to_date, company), as_dict=1)
|
and pp.company=%s""", (from_date, to_date, company), as_dict=1)
|
||||||
return payroll_period[0] if payroll_period else None
|
return payroll_period[0] if payroll_period else None
|
||||||
|
|
||||||
|
|
||||||
|
def allocate_earned_leaves():
|
||||||
|
'''Allocate earned leaves to Employees'''
|
||||||
|
e_leave_types = frappe.get_all("Leave Type",
|
||||||
|
fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"],
|
||||||
|
filters={'is_earned_leave' : 1})
|
||||||
|
today = getdate()
|
||||||
|
divide_by_frequency = {"Yearly": 1, "Quarterly": 4, "Monthly": 12}
|
||||||
|
if e_leave_types:
|
||||||
|
for e_leave_type in e_leave_types:
|
||||||
|
leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where '{0}'
|
||||||
|
between from_date and to_date and docstatus=1 and leave_type='{1}'"""
|
||||||
|
.format(today, e_leave_type.name), as_dict=1)
|
||||||
|
for allocation in leave_allocations:
|
||||||
|
leave_policy = get_employee_leave_policy(allocation.employee)
|
||||||
|
if not leave_policy:
|
||||||
|
continue
|
||||||
|
if not e_leave_type.earned_leave_frequency == "Monthly":
|
||||||
|
if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
|
||||||
|
continue
|
||||||
|
annual_allocation = frappe.db.sql("""select annual_allocation from `tabLeave Policy Detail`
|
||||||
|
where parent=%s and leave_type=%s""", (leave_policy.name, e_leave_type.name))
|
||||||
|
if annual_allocation and annual_allocation[0]:
|
||||||
|
earned_leaves = flt(annual_allocation[0][0]) / divide_by_frequency[e_leave_type.earned_leave_frequency]
|
||||||
|
if e_leave_type.rounding == "0.5":
|
||||||
|
earned_leaves = round(earned_leaves * 2) / 2
|
||||||
|
else:
|
||||||
|
earned_leaves = round(earned_leaves)
|
||||||
|
|
||||||
|
allocated_leaves = frappe.db.get_value('Leave Allocation', allocation.name, 'total_leaves_allocated')
|
||||||
|
new_allocation = flt(allocated_leaves) + flt(earned_leaves)
|
||||||
|
new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed
|
||||||
|
frappe.db.set_value('Leave Allocation', allocation.name, 'total_leaves_allocated', new_allocation)
|
||||||
|
|
||||||
|
def check_frequency_hit(from_date, to_date, frequency):
|
||||||
|
'''Return True if current date matches frequency'''
|
||||||
|
from_dt = get_datetime(from_date)
|
||||||
|
to_dt = get_datetime(to_date)
|
||||||
|
from dateutil import relativedelta
|
||||||
|
rd = relativedelta.relativedelta(to_dt, from_dt)
|
||||||
|
months = rd.months
|
||||||
|
if frequency == "Quarterly":
|
||||||
|
if not months % 3:
|
||||||
|
return True
|
||||||
|
elif frequency == "Yearly":
|
||||||
|
if not months % 12:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user