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