feat: Leave policy assignment (#23112)
* feat: Leave Policy Assignment
* feat: linking with leave allocation and valiations
* style: removed old code from leave period
* feat: Bulk Leave policy Assignment and grant Leaves
* fix: overlap validation
* feat: earned leaves based on joining date
* feat: automatic grant leave based on leave policy
* patch: create leave policy assignment based on employee current leave policy
* fix: dependent test cases
* test: Leave policy assignment
* fix: some enhancement
* style: break large function into small function
* fix:requested Changes
* fix(patch): Handled old Leave allocatioln
* fix:codacy
* fix: travis and sider,codacy
* fix: codacy
* fix: codacy
* fix: requested changes and sider
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 8d95924..d700e7f 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -215,19 +215,6 @@
+ _(") for {0}").format(exists_for)
frappe.throw(msg)
-def get_employee_leave_policy(employee):
- leave_policy = frappe.db.get_value("Employee", employee, "leave_policy")
- if not leave_policy:
- employee_grade = frappe.db.get_value("Employee", employee, "grade")
- if employee_grade:
- leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy")
- if not leave_policy:
- frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade))
- if leave_policy:
- return frappe.get_doc("Leave Policy", leave_policy)
- else:
- frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee))
-
def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
existing_record = frappe.db.exists(doctype, {
"payroll_period": payroll_period,
@@ -300,43 +287,68 @@
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})
+ e_leave_types = get_earned_leaves()
today = getdate()
- divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
for e_leave_type in e_leave_types:
- leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s
- between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1)
+
+ leave_allocations = get_leave_allocations(today, e_leave_type.name)
+
for allocation in leave_allocations:
- leave_policy = get_employee_leave_policy(allocation.employee)
- if not leave_policy:
+
+ if not allocation.leave_policy_assignment and not allocation.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
+
+ leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value(
+ "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"])
+
annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
- 'parent': leave_policy.name,
+ 'parent': leave_policy,
'leave_type': e_leave_type.name
}, fieldname=['annual_allocation'])
- if annual_allocation:
- earned_leaves = flt(annual_allocation) / 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)
- allocation = frappe.get_doc('Leave Allocation', allocation.name)
- new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
+ from_date=allocation.from_date
- if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
- new_allocation = e_leave_type.max_leaves_allowed
+ if e_leave_type.based_on_date_of_joining_date:
+ from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
- if new_allocation == allocation.total_leaves_allocated:
- continue
- allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
- create_additional_leave_ledger_entry(allocation, earned_leaves, today)
+ if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
+ update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
+
+def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
+ divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
+ if annual_allocation:
+ earned_leaves = flt(annual_allocation) / 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)
+
+ allocation = frappe.get_doc('Leave Allocation', allocation.name)
+ new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
+
+ if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
+ new_allocation = e_leave_type.max_leaves_allowed
+
+ if new_allocation != allocation.total_leaves_allocated:
+ allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
+ today_date = today()
+ create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
+
+def get_leave_allocations(date, leave_type):
+ return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
+ from `tabLeave Allocation`
+ where
+ %s between from_date and to_date and docstatus=1
+ and leave_type=%s""",
+ (date, leave_type), as_dict=1)
+
+
+def get_earned_leaves():
+ return frappe.get_all("Leave Type",
+ fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"],
+ filters={'is_earned_leave' : 1})
def create_additional_leave_ledger_entry(allocation, leaves, date):
''' Create leave ledger entry for leave types '''
@@ -345,24 +357,32 @@
allocation.unused_leaves = 0
allocation.create_leave_ledger_entry()
-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)
+def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date):
+ import calendar
from dateutil import relativedelta
- rd = relativedelta.relativedelta(to_dt, from_dt)
- months = rd.months
- if frequency == "Quarterly":
- if not months % 3:
+
+ from_date = get_datetime(from_date)
+ to_date = get_datetime(to_date)
+ rd = relativedelta.relativedelta(to_date, from_date)
+ #last day of month
+ last_day = calendar.monthrange(to_date.year, to_date.month)[1]
+
+ if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day):
+ if frequency == "Monthly":
return True
- elif frequency == "Half-Yearly":
- if not months % 6:
+ elif frequency == "Quarterly" and rd.months % 3:
return True
- elif frequency == "Yearly":
- if not months % 12:
+ elif frequency == "Half-Yearly" and rd.months % 6:
return True
+ elif frequency == "Yearly" and rd.months % 12:
+ return True
+
+ if frappe.flags.in_test:
+ return True
+
return False
+
def get_salary_assignment(employee, date):
assignment = frappe.db.sql("""
select * from `tabSalary Structure Assignment`
@@ -454,3 +474,10 @@
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount
+
+def grant_leaves_automatically():
+ automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
+ if automatically_allocate_leaves_based_on_leave_policy:
+ lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
+ for assignment in lpa:
+ frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()