From be286fa086dd991cfc5c3d3a2cc659746aec710f Mon Sep 17 00:00:00 2001 From: krantheman Date: Fri, 5 Jul 2024 17:02:06 +0530 Subject: [PATCH 1/5] feat(Earned Leave): allocate leaves manually --- .../leave_allocation/leave_allocation.js | 74 ++++++++++++++++++- .../leave_allocation/leave_allocation.py | 36 ++++++++- hrms/hr/utils.py | 1 + 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/hrms/hr/doctype/leave_allocation/leave_allocation.js b/hrms/hr/doctype/leave_allocation/leave_allocation.js index ad97491b73..6ca85faa2d 100755 --- a/hrms/hr/doctype/leave_allocation/leave_allocation.js +++ b/hrms/hr/doctype/leave_allocation/leave_allocation.js @@ -43,12 +43,81 @@ frappe.ui.form.on("Leave Allocation", { if (!frm.doc.__islocal && frm.doc.leave_policy_assignment) { frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => { - if (cint(r?.is_earned_leave)) - frm.set_df_property("new_leaves_allocated", "read_only", 1); + if (!r?.is_earned_leave) return; + frm.set_df_property("new_leaves_allocated", "read_only", 1); + frm.trigger("add_allocate_leaves_button"); }); } }, + add_allocate_leaves_button: async function (frm) { + const monthly_earned_leave = await frm.trigger("get_monthly_earned_leave"); + + frm.add_custom_button(__("Allocate Leaves Manually"), function () { + const dialog = new frappe.ui.Dialog({ + title: "Enter details", + fields: [ + { + label: "New Leaves to be Allocated", + fieldname: "new_leaves", + fieldtype: "Float", + }, + ], + primary_action_label: "Allocate", + primary_action({ new_leaves }) { + frappe.call({ + method: "allocate_leaves_manually", + doc: frm.doc, + args: { new_leaves }, + callback: function () { + frm.reload_doc(); + }, + }); + dialog.hide(); + }, + }); + dialog.fields_dict.new_leaves.set_value(monthly_earned_leave); + dialog.show(); + }); + }, + + get_monthly_earned_leave: async function (frm) { + const { + message: { date_of_joining }, + } = await frappe.db.get_value("Employee", frm.doc.employee, "date_of_joining"); + + const { + message: { annual_allocation }, + } = await frappe.db.get_value( + "Leave Policy Detail", + { + parent: frm.doc.leave_policy, + leave_type: frm.doc.leave_type, + }, + "annual_allocation", + () => {}, + "Leave Policy", + ); + + const { + message: { earned_leave_frequency, rounding }, + } = await frappe.db.get_value("Leave Type", frm.doc.leave_type, [ + "earned_leave_frequency", + "rounding", + ]); + + const { message } = await frappe.call({ + method: "hrms.hr.utils.get_monthly_earned_leave", + args: { + date_of_joining: date_of_joining, + annual_leaves: annual_allocation, + frequency: earned_leave_frequency, + rounding: rounding, + }, + }); + return message; + }, + expire_allocation: function (frm) { frappe.call({ method: "hrms.hr.doctype.leave_ledger_entry.leave_ledger_entry.expire_allocation", @@ -110,6 +179,7 @@ frappe.ui.form.on("Leave Allocation", { ); } }, + calculate_total_leaves_allocated: function (frm) { if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) { return frappe.call({ diff --git a/hrms/hr/doctype/leave_allocation/leave_allocation.py b/hrms/hr/doctype/leave_allocation/leave_allocation.py index 2631f691db..000d3b9ba5 100755 --- a/hrms/hr/doctype/leave_allocation/leave_allocation.py +++ b/hrms/hr/doctype/leave_allocation/leave_allocation.py @@ -12,7 +12,7 @@ create_leave_ledger_entry, expire_allocation, ) -from hrms.hr.utils import get_leave_period, set_employee_name +from hrms.hr.utils import create_additional_leave_ledger_entry, get_leave_period, set_employee_name class OverlapError(frappe.ValidationError): @@ -312,6 +312,40 @@ def create_leave_ledger_entry(self, submit=True): ) create_leave_ledger_entry(self, args, submit) + @frappe.whitelist() + def allocate_leaves_manually(self, new_leaves): + new_allocation = flt(self.total_leaves_allocated) + flt(new_leaves) + new_allocation_without_cf = flt( + flt(self.get_existing_leave_count()) + flt(new_leaves), + self.precision("total_leaves_allocated"), + ) + + max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + if new_allocation > max_leaves_allowed and max_leaves_allowed > 0: + new_allocation = max_leaves_allowed + + annual_allocation = frappe.db.get_value( + "Leave Policy Detail", + {"parent": self.leave_policy, "leave_type": self.leave_type}, + "annual_allocation", + ) + annual_allocation = flt(annual_allocation, self.precision("total_leaves_allocated")) + + if ( + new_allocation != self.total_leaves_allocated + # annual allocation as per policy should not be exceeded + and new_allocation_without_cf <= annual_allocation + ): + self.db_set("total_leaves_allocated", new_allocation, update_modified=False) + + date = frappe.flags.current_date or getdate() + create_additional_leave_ledger_entry(self, new_leaves, date) + + text = _("{0} leaves were manually allocated by {1} on {2}").format( + frappe.bold(new_leaves), frappe.session.user, frappe.bold(formatdate(date)) + ) + self.add_comment(comment_type="Info", text=text) + def get_previous_allocation(from_date, leave_type, employee): """Returns document properties of previous allocation""" diff --git a/hrms/hr/utils.py b/hrms/hr/utils.py index aaf1080089..55c3f37676 100644 --- a/hrms/hr/utils.py +++ b/hrms/hr/utils.py @@ -412,6 +412,7 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type allocation.add_comment(comment_type="Info", text=text) +@frappe.whitelist() def get_monthly_earned_leave( date_of_joining, annual_leaves, From 6c226d2e28aa8d5228b75454a7906ba2cf2e27c7 Mon Sep 17 00:00:00 2001 From: krantheman Date: Wed, 10 Jul 2024 11:34:58 +0530 Subject: [PATCH 2/5] refactor: move get_monthly_earned_leave to server side --- .../leave_allocation/leave_allocation.js | 42 ++----------------- .../leave_allocation/leave_allocation.py | 25 +++++++++++ 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/hrms/hr/doctype/leave_allocation/leave_allocation.js b/hrms/hr/doctype/leave_allocation/leave_allocation.js index 6ca85faa2d..9be93bb2a1 100755 --- a/hrms/hr/doctype/leave_allocation/leave_allocation.js +++ b/hrms/hr/doctype/leave_allocation/leave_allocation.js @@ -51,7 +51,10 @@ frappe.ui.form.on("Leave Allocation", { }, add_allocate_leaves_button: async function (frm) { - const monthly_earned_leave = await frm.trigger("get_monthly_earned_leave"); + const { message: monthly_earned_leave } = await frappe.call({ + method: "get_monthly_earned_leave", + doc: frm.doc, + }); frm.add_custom_button(__("Allocate Leaves Manually"), function () { const dialog = new frappe.ui.Dialog({ @@ -81,43 +84,6 @@ frappe.ui.form.on("Leave Allocation", { }); }, - get_monthly_earned_leave: async function (frm) { - const { - message: { date_of_joining }, - } = await frappe.db.get_value("Employee", frm.doc.employee, "date_of_joining"); - - const { - message: { annual_allocation }, - } = await frappe.db.get_value( - "Leave Policy Detail", - { - parent: frm.doc.leave_policy, - leave_type: frm.doc.leave_type, - }, - "annual_allocation", - () => {}, - "Leave Policy", - ); - - const { - message: { earned_leave_frequency, rounding }, - } = await frappe.db.get_value("Leave Type", frm.doc.leave_type, [ - "earned_leave_frequency", - "rounding", - ]); - - const { message } = await frappe.call({ - method: "hrms.hr.utils.get_monthly_earned_leave", - args: { - date_of_joining: date_of_joining, - annual_leaves: annual_allocation, - frequency: earned_leave_frequency, - rounding: rounding, - }, - }); - return message; - }, - expire_allocation: function (frm) { frappe.call({ method: "hrms.hr.doctype.leave_ledger_entry.leave_ledger_entry.expire_allocation", diff --git a/hrms/hr/doctype/leave_allocation/leave_allocation.py b/hrms/hr/doctype/leave_allocation/leave_allocation.py index 000d3b9ba5..99a80ff30d 100755 --- a/hrms/hr/doctype/leave_allocation/leave_allocation.py +++ b/hrms/hr/doctype/leave_allocation/leave_allocation.py @@ -13,6 +13,7 @@ expire_allocation, ) from hrms.hr.utils import create_additional_leave_ledger_entry, get_leave_period, set_employee_name +from hrms.hr.utils import get_monthly_earned_leave as _get_monthly_earned_leave class OverlapError(frappe.ValidationError): @@ -346,6 +347,30 @@ def allocate_leaves_manually(self, new_leaves): ) self.add_comment(comment_type="Info", text=text) + @frappe.whitelist() + def get_monthly_earned_leave(self): + doj = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + annual_allocation = frappe.db.get_value( + "Leave Policy Detail", + { + "parent": self.leave_policy, + "leave_type": self.leave_type, + }, + "annual_allocation", + ) + + frequency, rounding = frappe.db.get_value( + "Leave Type", + self.leave_type, + [ + "earned_leave_frequency", + "rounding", + ], + ) + + return _get_monthly_earned_leave(doj, annual_allocation, frequency, rounding) + def get_previous_allocation(from_date, leave_type, employee): """Returns document properties of previous allocation""" From 0b1e276f64b1a61b0fdf1f4d5e33864f2825926a Mon Sep 17 00:00:00 2001 From: krantheman Date: Wed, 25 Sep 2024 15:13:09 +0530 Subject: [PATCH 3/5] feat: add error message for annual allocation exceeded --- hrms/hr/doctype/leave_allocation/leave_allocation.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hrms/hr/doctype/leave_allocation/leave_allocation.py b/hrms/hr/doctype/leave_allocation/leave_allocation.py index 99a80ff30d..61b90939ac 100755 --- a/hrms/hr/doctype/leave_allocation/leave_allocation.py +++ b/hrms/hr/doctype/leave_allocation/leave_allocation.py @@ -347,6 +347,14 @@ def allocate_leaves_manually(self, new_leaves): ) self.add_comment(comment_type="Info", text=text) + else: + msg = _("Total leaves allocated cannot exceed annual allocation of {0}.").format( + frappe.bold(_(annual_allocation)) + ) + msg += "

" + msg += _("Reference: {0}").format(get_link_to_form("Leave Policy", self.leave_policy)) + frappe.throw(msg, title=_("Annual Allocation Exceeded")) + @frappe.whitelist() def get_monthly_earned_leave(self): doj = frappe.db.get_value("Employee", self.employee, "date_of_joining") From 676094370433bf98a37a9436b000853b1c381fa2 Mon Sep 17 00:00:00 2001 From: krantheman Date: Wed, 25 Sep 2024 16:33:16 +0530 Subject: [PATCH 4/5] test: allocate leaves manually --- .../leave_allocation/test_earned_leaves.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/leave_allocation/test_earned_leaves.py b/hrms/hr/doctype/leave_allocation/test_earned_leaves.py index 0dfdc01818..9f17827f81 100644 --- a/hrms/hr/doctype/leave_allocation/test_earned_leaves.py +++ b/hrms/hr/doctype/leave_allocation/test_earned_leaves.py @@ -3,8 +3,6 @@ from frappe.utils import ( add_days, add_months, - date_diff, - flt, get_first_day, get_last_day, get_year_ending, @@ -477,6 +475,32 @@ def test_get_earned_leave_details_for_dashboard(self): } self.assertEqual(leave_allocation, expected) + def test_allocate_leaves_manually(self): + frappe.flags.current_date = get_year_start(getdate()) + lpas = make_policy_assignment( + self.employee, + allocate_on_day="First Day", + start_date=frappe.flags.current_date, + ) + + leave_allocation = frappe.get_last_doc( + "Leave Allocation", filters={"leave_policy_assignment": lpas[0]} + ) + leave_allocation.allocate_leaves_manually(1) + leave_allocation.allocate_leaves_manually(1) + leave_allocation.allocate_leaves_manually(1) + leave_allocation.allocate_leaves_manually(1) + leave_allocation.allocate_leaves_manually(1) + self.assertEqual( + get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 6 + ) + + leave_allocation.allocate_leaves_manually(6) + self.assertEqual( + get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 12 + ) + self.assertRaises(frappe.ValidationError, leave_allocation.allocate_leaves_manually, 1) + def tearDown(self): frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) frappe.db.set_value("Leave Type", self.leave_type, "max_leaves_allowed", 0) From 6431facf4d88a9bc5324a042d2cd0f197c9df426 Mon Sep 17 00:00:00 2001 From: krantheman Date: Wed, 25 Sep 2024 17:13:18 +0530 Subject: [PATCH 5/5] refactor: group buttons under Actions tab --- .../leave_allocation/leave_allocation.js | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/hrms/hr/doctype/leave_allocation/leave_allocation.js b/hrms/hr/doctype/leave_allocation/leave_allocation.js index 8c6cffabda..b488ed6d29 100755 --- a/hrms/hr/doctype/leave_allocation/leave_allocation.js +++ b/hrms/hr/doctype/leave_allocation/leave_allocation.js @@ -35,9 +35,15 @@ frappe.ui.form.on("Leave Allocation", { ); if (valid_expiry) { // expire current allocation - frm.add_custom_button(__("Expire Allocation"), function () { - frm.trigger("expire_allocation"); - }); + frm.add_custom_button( + __("Expire Allocation"), + function () { + frappe.confirm("Are you sure you want to expire this allocation?", () => { + frm.trigger("expire_allocation"); + }); + }, + __("Actions"), + ); } } @@ -56,32 +62,36 @@ frappe.ui.form.on("Leave Allocation", { doc: frm.doc, }); - frm.add_custom_button(__("Allocate Leaves Manually"), function () { - const dialog = new frappe.ui.Dialog({ - title: "Enter details", - fields: [ - { - label: "New Leaves to be Allocated", - fieldname: "new_leaves", - fieldtype: "Float", - }, - ], - primary_action_label: "Allocate", - primary_action({ new_leaves }) { - frappe.call({ - method: "allocate_leaves_manually", - doc: frm.doc, - args: { new_leaves }, - callback: function () { - frm.reload_doc(); + frm.add_custom_button( + __("Allocate Leaves"), + function () { + const dialog = new frappe.ui.Dialog({ + title: "Manual Leave Allocation", + fields: [ + { + label: "New Leaves to be Allocated", + fieldname: "new_leaves", + fieldtype: "Float", }, - }); - dialog.hide(); - }, - }); - dialog.fields_dict.new_leaves.set_value(monthly_earned_leave); - dialog.show(); - }); + ], + primary_action_label: "Allocate", + primary_action({ new_leaves }) { + frappe.call({ + method: "allocate_leaves_manually", + doc: frm.doc, + args: { new_leaves }, + callback: function () { + frm.reload_doc(); + }, + }); + dialog.hide(); + }, + }); + dialog.fields_dict.new_leaves.set_value(monthly_earned_leave); + dialog.show(); + }, + __("Actions"), + ); }, expire_allocation: function (frm) {