From 375db6181db45db094254d4c669b1084aea1b318 Mon Sep 17 00:00:00 2001 From: Ranjith Kurungadam Date: Fri, 1 Jun 2018 16:09:28 +0530 Subject: [PATCH] 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 --- erpnext/hooks.py | 3 +- .../leave_allocation/leave_allocation.py | 47 +++++++++-------- .../test_leave_application.py | 45 +++++++++------- .../hr/doctype/leave_period/leave_period.py | 3 +- erpnext/hr/doctype/leave_type/leave_type.json | 3 +- erpnext/hr/utils.py | 51 ++++++++++++++++++- 6 files changed, 105 insertions(+), 47 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 938f7fa1875..815e2ebc2ab 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -238,7 +238,8 @@ scheduler_events = { "erpnext.assets.doctype.asset.asset.make_post_gl_entry" ], "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" ] } diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 8432cfe254a..7cffa4c5b1e 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -43,16 +43,16 @@ class LeaveAllocation(Document): def on_update_after_submit(self): self.validate_new_leaves_allocated_value() self.set_total_leaves_allocated() - + frappe.db.set(self,'carry_forwarded_leaves', flt(self.carry_forwarded_leaves)) frappe.db.set(self,'total_leaves_allocated',flt(self.total_leaves_allocated)) - + self.validate_against_leave_applications() def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: frappe.throw(_("To date cannot be before from date")) - + def validate_lwp(self): if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"): frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type)) @@ -66,45 +66,45 @@ class LeaveAllocation(Document): leave_allocation = frappe.db.sql(""" select name from `tabLeave Allocation` where employee=%s and leave_type=%s and docstatus=1 - and to_date >= %s and from_date <= %s""", + and to_date >= %s and from_date <= %s""", (self.employee, self.leave_type, self.from_date, self.to_date)) if leave_allocation: frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") .format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date))) - + frappe.throw(_('Reference') + ': {0}' .format(leave_allocation[0][0]), OverlapError) - + def validate_back_dated_allocation(self): future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 and from_date > %s + where employee=%s and leave_type=%s and docstatus=1 and from_date > %s and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) - + if future_allocation: frappe.throw(_("Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") - .format(formatdate(future_allocation[0].from_date), future_allocation[0].name), + .format(formatdate(future_allocation[0].from_date), future_allocation[0].name), BackDatedAllocationError) def set_total_leaves_allocated(self): - self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee, + self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee, self.leave_type, self.from_date, self.carry_forward) - + self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated) - - if not self.total_leaves_allocated: - frappe.throw(_("Total leaves allocated is mandatory")) + + 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 for Leave Type {0}".format(self.leave_type))) def validate_total_leaves_allocated(self): # Adding a day to include To Date in the difference date_difference = date_diff(self.to_date, self.from_date) + 1 if date_difference < self.total_leaves_allocated: frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError) - + def validate_against_leave_applications(self): - leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, + leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, self.from_date, self.to_date) - + if flt(leaves_taken) > flt(self.total_leaves_allocated): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) @@ -137,10 +137,10 @@ def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): @frappe.whitelist() def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): carry_forwarded_leaves = 0 - + if carry_forward: validate_carry_forward(leave_type) - + previous_allocation = frappe.db.sql(""" select name, from_date, to_date, total_leaves_allocated from `tabLeave Allocation` @@ -148,14 +148,13 @@ def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): order by to_date desc limit 1 """, (employee, leave_type, date), as_dict=1) if previous_allocation: - leaves_taken = get_approved_leaves_for_period(employee, leave_type, + leaves_taken = get_approved_leaves_for_period(employee, leave_type, previous_allocation[0].from_date, previous_allocation[0].to_date) - + carry_forwarded_leaves = flt(previous_allocation[0].total_leaves_allocated) - flt(leaves_taken) - + return carry_forwarded_leaves - + def validate_carry_forward(leave_type): 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)) - diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index b4f4c1ca931..5506d608e65 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -7,7 +7,7 @@ import unittest 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.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"] @@ -387,25 +387,32 @@ class TestLeaveApplication(unittest.TestCase): self.assertRaises(frappe.ValidationError, leave_application.insert) - # def test_earned_leave(self): - # leave_period = get_leave_period() - # 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) + def test_earned_leave(self): + leave_period = get_leave_period() + 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, + 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): frappe.db.sql("delete from `tabLeave Allocation`") diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index 4097169ad82..39001ee7b70 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -71,7 +71,8 @@ class LeavePeriod(Document): allocation.leave_type = leave_type allocation.from_date = self.from_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 if self.carry_forward_leaves: if frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index ef66a0a819d..1e0b0488dc3 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -584,7 +584,7 @@ "default": "0.5", "depends_on": "is_earned_leave", "fieldname": "rounding", - "fieldtype": "Float", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -595,6 +595,7 @@ "label": "Rounding", "length": 0, "no_copy": 0, + "options": "0.5\n1.0", "permlevel": 0, "precision": "", "print_hide": 0, diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 20fe666d2b1..4e937c60a5f 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe 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.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 and pp.company=%s""", (from_date, to_date, company), as_dict=1) 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