diff --git a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
index 7c06584fcd..4d83433ab2 100644
--- a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
+++ b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
@@ -22,6 +22,24 @@ frappe.ui.form.on("Leave Policy Assignment", {
});
},
+ before_submit: async function (frm) {
+ if (frm.doc.mid_period_change) {
+ const promise = new Promise((resolve, reject) => {
+ frappe.confirm(
+ __(
+ "Are you sure you want to apply the new leave policy in the middle of current policy assignment period? This change will take effect immediately and cannot be undone.",
+ ),
+ resolve,
+ reject,
+ );
+ });
+ await promise.catch(() => {
+ $(".primary-action").prop("disabled", false);
+ frappe.throw(__("Please untick Allow Mid-Period Policy Change checkbox."));
+ });
+ }
+ },
+
assignment_based_on: function (frm) {
if (frm.doc.assignment_based_on) {
frm.events.set_effective_date(frm);
diff --git a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index 2291ef5e22..d445a03701 100644
--- a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -17,6 +17,7 @@
"effective_from",
"effective_to",
"leaves_allocated",
+ "mid_period_change",
"amended_from"
],
"fields": [
@@ -109,11 +110,17 @@
"label": "Leaves Allocated",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "mid_period_change",
+ "fieldtype": "Check",
+ "label": "Allow mid-period policy change"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:10:01.746553",
+ "modified": "2024-06-20 16:11:45.975901",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
diff --git a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index 9fab7276b6..3115dd3c90 100644
--- a/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -8,6 +8,7 @@
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import (
+ add_days,
add_months,
cint,
comma_and,
@@ -22,6 +23,10 @@
)
+class LeaveAcrossAllocationsMidPeriodError(frappe.ValidationError):
+ pass
+
+
class LeavePolicyAssignment(Document):
def validate(self):
self.set_dates()
@@ -40,6 +45,8 @@ def set_dates(self):
self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
def validate_policy_assignment_overlap(self):
+ if self.mid_period_change:
+ return
leave_policy_assignment = frappe.db.get_value(
"Leave Policy Assignment",
{
@@ -82,6 +89,9 @@ def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
else:
+ if self.mid_period_change:
+ self.validate_leave_application_across_allocations()
+ self.end_existing_policy_assignment()
leave_allocations = {}
leave_type_details = get_leave_type_details()
@@ -104,6 +114,67 @@ def grant_leave_alloc_for_employee(self):
self.db_set("leaves_allocated", 1)
return leave_allocations
+ def end_existing_policy_assignment(self):
+ leave_policy_assignment_name = frappe.db.exists(
+ "Leave Policy Assignment",
+ {
+ "employee": self.employee,
+ "name": ("!=", self.name),
+ "docstatus": 1,
+ "effective_to": (">=", self.effective_from),
+ "effective_from": ("<=", self.effective_to),
+ },
+ )
+ if leave_policy_assignment_name:
+ end_date = add_days(self.effective_from, -1)
+ leave_allocations = frappe.get_all(
+ "Leave Allocation",
+ filters={
+ "leave_policy_assignment": leave_policy_assignment_name,
+ "docstatus": 1,
+ },
+ pluck="name",
+ )
+
+ frappe.db.set_value(
+ "Leave Policy Assignment", leave_policy_assignment_name, "effective_to", end_date
+ )
+ for allocation in leave_allocations:
+ frappe.db.set_value("Leave Allocation", allocation, "to_date", end_date)
+ frappe.db.set_value(
+ "Leave Ledger Entry",
+ {
+ "transaction_name": allocation,
+ "docstatus": 1,
+ },
+ "to_date",
+ end_date,
+ )
+
+ def validate_leave_application_across_allocations(self):
+ leave_applications = frappe.get_all(
+ "Leave Application",
+ filters={
+ "employee": self.employee,
+ "docstatus": 1,
+ "status": "Approved",
+ "from_date": ("<=", self.effective_from),
+ "to_date": (">=", self.effective_from),
+ },
+ pluck="name",
+ )
+ if leave_applications:
+ frappe.throw(
+ _(
+ "Leave Application period cannot be across two allocation records. Please cancel the following Leave Application records:
"
+ ).format(
+ "
".join(
+ [get_link_to_form("Leave Application", d, d) for d in leave_applications]
+ )
+ ),
+ exc=LeaveAcrossAllocationsMidPeriodError,
+ )
+
def create_leave_allocation(self, annual_allocation, leave_details, date_of_joining):
# Creates leave allocation for the given employee in the provided leave period
carry_forward = self.carry_forward
@@ -113,18 +184,16 @@ def create_leave_allocation(self, annual_allocation, leave_details, date_of_join
new_leaves_allocated = self.get_new_leaves(annual_allocation, leave_details, date_of_joining)
allocation = frappe.get_doc(
- dict(
- doctype="Leave Allocation",
- employee=self.employee,
- leave_type=leave_details.name,
- from_date=self.effective_from,
- to_date=self.effective_to,
- new_leaves_allocated=new_leaves_allocated,
- leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else "",
- leave_policy_assignment=self.name,
- leave_policy=self.leave_policy,
- carry_forward=carry_forward,
- )
+ doctype="Leave Allocation",
+ employee=self.employee,
+ leave_type=leave_details.name,
+ from_date=self.effective_from,
+ to_date=self.effective_to,
+ new_leaves_allocated=new_leaves_allocated,
+ leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else "",
+ leave_policy_assignment=self.name,
+ leave_policy=self.leave_policy,
+ carry_forward=carry_forward,
)
allocation.save(ignore_permissions=True)
allocation.submit()
diff --git a/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index ecece69b48..d0490b0422 100644
--- a/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/hrms/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -1,13 +1,20 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
+import datetime
+
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import add_months, get_first_day, get_year_ending, getdate
+from frappe.utils import add_days, add_months, get_first_day, get_year_ending, get_year_start, getdate
-from hrms.hr.doctype.leave_application.test_leave_application import get_employee, get_leave_period
+from hrms.hr.doctype.leave_application.test_leave_application import (
+ get_employee,
+ get_leave_balance_on,
+ get_leave_period,
+)
from hrms.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
from hrms.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
+ LeaveAcrossAllocationsMidPeriodError,
create_assignment_for_multiple_employees,
)
@@ -161,5 +168,115 @@ def test_pro_rated_leave_allocation_for_custom_date_range(self):
self.assertGreater(new_leaves_allocated, 0)
+ def test_mid_period_leave_policy_change(self):
+ leave_type = frappe.get_doc(
+ {
+ "doctype": "Leave Type",
+ "leave_type_name": "_Test Leave Type Mid Period Policy Change",
+ "include_holiday": 1,
+ }
+ ).insert()
+ leave_policy_1 = create_leave_policy(annual_allocation=6, leave_type=leave_type.name).submit()
+ leave_policy_2 = create_leave_policy(annual_allocation=12, leave_type=leave_type.name).submit()
+
+ today_date = getdate()
+ year_start = getdate(get_year_start(today_date))
+ year_end = getdate(get_year_ending(today_date))
+ leave_policy_assignment = create_leave_policy_assignment(
+ self.employee.name,
+ leave_policy_1.name,
+ year_start,
+ year_end,
+ ).submit()
+
+ new_assignment_date = add_months(year_start, 6)
+ new_leave_policy_assignment = create_leave_policy_assignment(
+ self.employee.name,
+ leave_policy_2.name,
+ new_assignment_date,
+ year_end,
+ )
+ new_leave_policy_assignment.mid_period_change = True
+
+ new_leave_policy_assignment.submit()
+
+ leave_allocation_name = frappe.db.exists(
+ "Leave Allocation",
+ {
+ "leave_policy_assignment": leave_policy_assignment.name,
+ "docstatus": 1,
+ },
+ )
+ leave_allocation = frappe.get_doc("Leave Allocation", leave_allocation_name)
+ leave_ledger_entry_to_date = frappe.db.get_value(
+ "Leave Ledger Entry",
+ {
+ "transaction_name": leave_allocation_name,
+ "docstatus": 1,
+ },
+ "to_date",
+ )
+ leave_policy_assignment = leave_policy_assignment.reload()
+ end_date = add_days(new_leave_policy_assignment.effective_from, -1)
+
+ self.assertEqual(getdate(leave_policy_assignment.effective_to), end_date)
+ self.assertEqual(getdate(leave_allocation.to_date), end_date)
+ self.assertEqual(getdate(leave_ledger_entry_to_date), end_date)
+ self.assertEqual(
+ get_leave_balance_on(self.employee.name, leave_type.name, add_days(new_assignment_date, 1)),
+ 12,
+ )
+
+ def test_leave_across_allocations_mid_period_leave_policy_change(self):
+ employee = frappe.get_doc("Employee", "_T-Employee-00002")
+ leave_type = frappe.get_doc(
+ {
+ "doctype": "Leave Type",
+ "leave_type_name": "_Test Leave Type Across Mid Period Policy Change",
+ }
+ ).insert()
+ leave_policy_1 = create_leave_policy(leave_type=leave_type.name).submit()
+ leave_policy_2 = create_leave_policy(eave_type=leave_type.name).submit()
+
+ year_start = datetime.date(getdate().year + 1, 1, 1)
+ year_end = getdate(get_year_ending(year_start))
+ create_leave_policy_assignment(
+ employee.name,
+ leave_policy_1.name,
+ year_start,
+ year_end,
+ ).submit()
+
+ new_assignment_date = add_months(year_start, 6)
+ leave_application = frappe.get_doc(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(new_assignment_date, -1),
+ to_date=add_days(new_assignment_date, 1),
+ company="_Test Company",
+ status="Approved",
+ leave_approver="test@example.com",
+ )
+ leave_application.submit()
+ new_leave_policy_assignment = create_leave_policy_assignment(
+ employee.name,
+ leave_policy_2.name,
+ new_assignment_date,
+ year_end,
+ )
+ new_leave_policy_assignment.mid_period_change = True
+ # Application period cannot be across two allocation records
+ self.assertRaises(LeaveAcrossAllocationsMidPeriodError, new_leave_policy_assignment.submit)
+
def tearDown(self):
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
+
+
+def create_leave_policy_assignment(employee, leave_policy, effective_from, effective_to):
+ leave_policy_assignment = frappe.new_doc("Leave Policy Assignment")
+ leave_policy_assignment.employee = employee
+ leave_policy_assignment.leave_policy = leave_policy
+ leave_policy_assignment.effective_from = effective_from
+ leave_policy_assignment.effective_to = effective_to
+ return leave_policy_assignment