Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow mid-period leave policy change #1910

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <b style='color:red;'>in the middle of current policy assignment period</b>? This change will take effect immediately and cannot be undone.",
),
resolve,
reject,
);
});
await promise.catch(() => {
$(".primary-action").prop("disabled", false);
frappe.throw(__("Please untick <b>Allow Mid-Period Policy Change</b> checkbox."));
});
}
},

assignment_based_on: function (frm) {
if (frm.doc.assignment_based_on) {
frm.events.set_effective_date(frm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"effective_from",
"effective_to",
"leaves_allocated",
"mid_period_change",
"amended_from"
],
"fields": [
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,6 +23,10 @@
)


class LeaveAcrossAllocationsMidPeriodError(frappe.ValidationError):
pass


class LeavePolicyAssignment(Document):
def validate(self):
self.set_dates()
Expand All @@ -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",
{
Expand Down Expand Up @@ -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()

Expand All @@ -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:<br/><b><ul><li>{0}</li></ul></b>"
).format(
"</li><li>".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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)

Expand Down Expand Up @@ -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
Loading