From decc64723e3a5f843ac06fea0c0b8fc1952ba36a Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Thu, 9 Apr 2026 16:07:15 +0530 Subject: [PATCH 1/4] feat: leave application submission through calendar view dialog --- .../leave_application_calendar.js | 643 ++++++++++++++++++ 1 file changed, 643 insertions(+) diff --git a/hrms/hr/doctype/leave_application/leave_application_calendar.js b/hrms/hr/doctype/leave_application/leave_application_calendar.js index 6fc85da2e2..0c41f0b260 100644 --- a/hrms/hr/doctype/leave_application/leave_application_calendar.js +++ b/hrms/hr/doctype/leave_application/leave_application_calendar.js @@ -1,6 +1,518 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +const HIDDEN_QUICK_ENTRY_FIELDS = ["status", "posting_date", "naming_series"]; + +function set_hidden_defaults(quick_entry) { + quick_entry.doc.status = quick_entry.doc.status || "Open"; + quick_entry.doc.posting_date = quick_entry.doc.posting_date || frappe.datetime.get_today(); + + const naming_series_field = quick_entry.get_field("naming_series"); + const default_series = + naming_series_field?.df.default || naming_series_field?.df.options?.split("\n")[0]; + + if (default_series) { + quick_entry.doc.naming_series = quick_entry.doc.naming_series || default_series; + } +} + +function hide_field_completely(quick_entry, fieldname) { + quick_entry.set_df_property(fieldname, "hidden", 1); + quick_entry.get_field(fieldname)?.$wrapper?.hide(); +} + +function get_extra_fields_container(quick_entry) { + if (!quick_entry._extra_fields_container) { + quick_entry._extra_fields_container = $( + "
", + ); + const anchor = quick_entry._date_fields_row || quick_entry.get_field("to_date")?.$wrapper; + + if (anchor?.after) { + anchor.after(quick_entry._extra_fields_container); + } else { + quick_entry.$body?.find(".form-layout")?.append(quick_entry._extra_fields_container); + } + } + + return quick_entry._extra_fields_container; +} + +function ensure_reason_field(quick_entry) { + const extra_fields_container = get_extra_fields_container(quick_entry); + const reason_df = frappe.meta.get_docfield("Leave Application", "description"); + if (reason_df && extra_fields_container?.get?.(0)) { + quick_entry._reason_control = frappe.ui.form.make_control({ + df: { + ...reason_df, + label: __("Reason"), + hidden: 0, + reqd: 0, + placeholder: __("Provide a short reason for leave."), + }, + parent: extra_fields_container.get(0), + render_input: true, + doc: quick_entry.dialog.doc, + }); + + quick_entry._reason_control?.$wrapper?.addClass("mt-2"); + quick_entry._reason_control?.$input?.on("input change", () => { + const value = quick_entry._reason_control?.get_value?.() || ""; + quick_entry.doc.description = value; + quick_entry.dialog.doc.description = value; + }); + } + + if (quick_entry._reason_control?.$wrapper) { + extra_fields_container?.append(quick_entry._reason_control.$wrapper); + } + + const value = quick_entry.doc.description || quick_entry.dialog.doc.description || ""; + quick_entry._reason_control?.set_value?.(value, true); + quick_entry.doc.description = value; + quick_entry.dialog.doc.description = value; +} + +function sync_reason_value(quick_entry) { + const raw_value = quick_entry._reason_control?.get_value?.(); + const value = + raw_value !== undefined && raw_value !== null + ? raw_value + : quick_entry.dialog.get_value("description") || quick_entry.doc.description || ""; + + quick_entry.doc.description = value; + quick_entry.dialog.doc.description = value; + + return value; +} + +function ensure_total_leave_days_display(quick_entry) { + if (quick_entry._total_leave_days_control) { + return; + } + const extra_fields_container = get_extra_fields_container(quick_entry); + const total_leave_days_df = frappe.meta.get_docfield("Leave Application", "total_leave_days"); + if (!total_leave_days_df || !extra_fields_container?.get?.(0)) { + return; + } + + const control = frappe.ui.form.make_control({ + df: { + ...total_leave_days_df, + hidden: 0, + read_only: 1, + reqd: 0, + }, + parent: extra_fields_container.get(0), + render_input: true, + doc: quick_entry.dialog.doc, + }); + + control?.toggle_description?.(false); + control?.$wrapper?.addClass("mt-2"); + extra_fields_container.prepend(control.$wrapper); + quick_entry._total_leave_days_control = control; +} + +function set_total_leave_days_value(quick_entry, value) { + const has_value = value !== "" && value !== null && value !== undefined; + const numeric_value = has_value ? Number(value) : ""; + + if (quick_entry._total_leave_days_control?.set_value) { + quick_entry._total_leave_days_control.set_value(numeric_value, true); + } else { + quick_entry.set_value("total_leave_days", numeric_value); + } + + quick_entry.doc.total_leave_days = has_value ? numeric_value : 0; + quick_entry.dialog.doc.total_leave_days = has_value ? numeric_value : 0; +} + +function get_quick_entry_value(quick_entry, fieldname) { + const field = quick_entry.get_field(fieldname); + const value = quick_entry.dialog.get_value(fieldname); + + if (field && value !== undefined && value !== null) { + return value; + } + + if (value !== undefined && value !== null && value !== "") { + return value; + } + + return quick_entry.doc[fieldname]; +} + +function set_leave_type_summary(quick_entry, summary) { + const leave_type_wrapper = quick_entry.get_field("leave_type")?.$wrapper; + if (!leave_type_wrapper) { + return; + } + + if (!quick_entry._leave_type_summary) { + quick_entry._leave_type_summary = $("
"); + leave_type_wrapper.after(quick_entry._leave_type_summary); + } + + if (!summary) { + quick_entry._leave_type_summary.hide().text(""); + return; + } + + if (typeof summary === "string") { + const text = (summary || "").trim(); + if (!text) { + quick_entry._leave_type_summary.hide().text(""); + return; + } + + quick_entry._leave_type_summary + .html(`
${frappe.utils.escape_html(text)}
`) + .show(); + return; + } + + const allocated = frappe.utils.escape_html(summary.allocated || "0.0"); + const used = frappe.utils.escape_html(summary.used || "0.0"); + const remaining = frappe.utils.escape_html(summary.remaining || "0.0"); + + quick_entry._leave_type_summary + .html( + ` +
+
+
+ ${__("Allocated")} + ${allocated} +
+
+
+
+ ${__("Used")} + ${used} +
+
+
+
+ ${__("Remaining")} + ${remaining} +
+
+
+ `, + ) + .show(); +} + +function setup_date_fields_row(quick_entry) { + const from_date_field = quick_entry.get_field("from_date"); + const to_date_field = quick_entry.get_field("to_date"); + + if ( + !from_date_field?.$wrapper || + !to_date_field?.$wrapper || + quick_entry._date_row_setup_done + ) { + return; + } + + const row = $("
"); + from_date_field.$wrapper.before(row); + row.append(from_date_field.$wrapper); + row.append(to_date_field.$wrapper); + + from_date_field.$wrapper.addClass("col-sm-6 pr-1"); + to_date_field.$wrapper.addClass("col-sm-6 pl-1"); + + quick_entry._date_row_setup_done = true; + quick_entry._date_fields_row = row; +} + +function set_allowed_leave_types_query(quick_entry, allowed_leave_types) { + const allowed = Array.from(new Set((allowed_leave_types || []).filter(Boolean))); + quick_entry._allowed_leave_types = allowed; + + quick_entry.set_query("leave_type", () => { + if (!quick_entry._allowed_leave_types?.length) { + return {}; + } + + return { + filters: [["leave_type_name", "in", quick_entry._allowed_leave_types]], + }; + }); +} + +function refresh_allowed_leave_types(quick_entry) { + const employee = get_quick_entry_value(quick_entry, "employee"); + const date = + get_quick_entry_value(quick_entry, "from_date") || + quick_entry.doc.posting_date || + frappe.datetime.get_today(); + + if (!employee) { + set_allowed_leave_types_query(quick_entry, []); + return Promise.resolve(); + } + + const request_id = (quick_entry._leave_type_filter_request_id || 0) + 1; + quick_entry._leave_type_filter_request_id = request_id; + + return frappe + .call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_details", + args: { + employee, + date, + }, + }) + .then((r) => { + if (quick_entry._leave_type_filter_request_id !== request_id) { + return; + } + + const leave_allocation = r?.message?.leave_allocation || {}; + const lwps = r?.message?.lwps || []; + const allowed_leave_types = Object.keys(leave_allocation).concat(lwps); + + set_allowed_leave_types_query(quick_entry, allowed_leave_types); + + const current_leave_type = get_quick_entry_value(quick_entry, "leave_type"); + if (current_leave_type && !allowed_leave_types.includes(current_leave_type)) { + quick_entry.set_value("leave_type", ""); + } + }) + .catch(() => { + if (quick_entry._leave_type_filter_request_id !== request_id) { + return; + } + + set_allowed_leave_types_query(quick_entry, []); + }); +} + +function format_metric(value) { + return Number(value || 0).toFixed(1); +} + +function refresh_leave_metrics(quick_entry) { + const employee = get_quick_entry_value(quick_entry, "employee"); + const leave_type = get_quick_entry_value(quick_entry, "leave_type"); + const from_date = get_quick_entry_value(quick_entry, "from_date"); + const to_date = get_quick_entry_value(quick_entry, "to_date"); + ensure_total_leave_days_display(quick_entry); + + if (!employee || !leave_type || !from_date || !to_date) { + quick_entry.set_intro(""); + set_total_leave_days_value(quick_entry, ""); + set_leave_type_summary(quick_entry, ""); + return; + } + + const request_id = (quick_entry._leave_metrics_request_id || 0) + 1; + quick_entry._leave_metrics_request_id = request_id; + + Promise.all([ + frappe.call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_number_of_leave_days", + args: { + employee, + leave_type, + from_date, + to_date, + }, + }), + frappe.call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_details", + args: { + employee, + date: from_date, + }, + }), + ]) + .then(([days_response, details_response]) => { + if (quick_entry._leave_metrics_request_id !== request_id) { + return; + } + + const requested_days = days_response?.message; + const allocation = details_response?.message?.leave_allocation?.[leave_type] || {}; + const allocated = format_metric(allocation?.total_leaves); + const used = format_metric(allocation?.leaves_taken); + const remaining = format_metric(allocation?.remaining_leaves); + const has_allocation = Object.prototype.hasOwnProperty.call( + details_response?.message?.leave_allocation || {}, + leave_type, + ); + + set_leave_type_summary( + quick_entry, + has_allocation + ? { + allocated, + used, + remaining, + } + : "", + ); + set_total_leave_days_value(quick_entry, requested_days); + quick_entry.set_intro(""); + }) + .catch(() => { + if (quick_entry._leave_metrics_request_id !== request_id) { + return; + } + + quick_entry.set_intro(""); + set_total_leave_days_value(quick_entry, ""); + set_leave_type_summary( + quick_entry, + __("Unable to load leave allocation details right now."), + ); + }); +} + +function bind_link_change(quick_entry, fieldname, handler) { + const field = quick_entry.get_field(fieldname); + if (!field) { + return; + } + + const namespaced_events = ".leave_calendar_quick_entry_link"; + const trigger_handler = frappe.utils.debounce(() => handler(), 150); + + field.$input?.off(namespaced_events); + field.$input?.on( + `change${namespaced_events} awesomplete-selectcomplete${namespaced_events}`, + trigger_handler, + ); + + field.$wrapper?.off(namespaced_events); + field.$wrapper?.on( + `change${namespaced_events} awesomplete-selectcomplete${namespaced_events}`, + "input, .awesomplete input", + trigger_handler, + ); +} + +function bind_input_change(quick_entry, fieldname, handler) { + const field = quick_entry.get_field(fieldname); + if (!field) { + return; + } + + const namespaced_events = ".leave_calendar_quick_entry"; + const trigger_handler = frappe.utils.debounce(() => handler(), 150); + + field.$input?.off(namespaced_events); + field.$input?.on( + `change${namespaced_events} input${namespaced_events} blur${namespaced_events} awesomplete-selectcomplete${namespaced_events}`, + trigger_handler, + ); + + field.$wrapper?.off(namespaced_events); + field.$wrapper?.on( + `change${namespaced_events} awesomplete-selectcomplete${namespaced_events}`, + "input, .awesomplete input", + trigger_handler, + ); +} + +function set_leave_approver_value(quick_entry, leave_approver) { + const value = leave_approver || ""; + quick_entry.dialog.doc.leave_approver = value; + quick_entry.doc.leave_approver = value; + quick_entry.set_value("leave_approver", value); + return value; +} + +function get_quick_entry_employee(quick_entry) { + return ( + quick_entry.dialog.doc.employee || + quick_entry.doc.employee || + quick_entry.dialog.get_value("employee") + ); +} + +function setup_full_form_action(quick_entry) { + quick_entry.set_secondary_action_label(__("Open Full Form")); + quick_entry.set_secondary_action(async () => { + sync_reason_value(quick_entry); + + const employee = get_quick_entry_employee(quick_entry); + await sync_leave_approver(quick_entry, employee, { clear_existing: true }).catch(() => {}); + + quick_entry.open_doc(true); + }); +} + +function sync_leave_approver(quick_entry, employee, options = {}) { + const { clear_existing = false } = options; + + if (clear_existing) { + set_leave_approver_value(quick_entry, ""); + } + + return frappe + .call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_mandatory_approval", + args: { + doctype: "Leave Application", + }, + }) + .then((mandatory_response) => { + const is_mandatory = Number(mandatory_response?.message); + + if (!employee) { + set_leave_approver_value(quick_entry, ""); + return { is_mandatory, leave_approver: "" }; + } + + return frappe + .call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_approver", + args: { + employee, + }, + }) + .then((r) => { + const leave_approver = set_leave_approver_value(quick_entry, r?.message || ""); + return { is_mandatory, leave_approver }; + }); + }) + .then((result) => { + if (result) { + return result; + } + + return { is_mandatory: 0, leave_approver: "" }; + }); +} + +function ensure_leave_approver(quick_entry, employee, options = {}) { + const { clear_existing = false, enforce_mandatory = false } = options; + + return sync_leave_approver(quick_entry, employee, { clear_existing }).then( + ({ is_mandatory, leave_approver }) => { + if (enforce_mandatory && is_mandatory && !leave_approver) { + if (!employee) { + frappe.throw({ + title: __("Employee Required"), + message: __("Please select an Employee before continuing."), + }); + } + + frappe.throw({ + title: __("Leave Approver Missing"), + message: __("Please set Leave Approver for the Employee: {0}", [employee]), + }); + } + + return leave_approver; + }, + ); +} + frappe.views.calendar["Leave Application"] = { field_map: { start: "from_date", @@ -16,6 +528,137 @@ frappe.views.calendar["Leave Application"] = { center: "title", right: "month", }, + select: async function (info) { + const milliseconds = info.end - info.start; + + if (info.view.type === "dayGridMonth" && milliseconds === 86400000) { + return; + } + + const from_date = frappe.datetime.get_datetime_as_string(info.start).split(" ")[0]; + const to_date = frappe.datetime + .get_datetime_as_string(new Date(info.end.getTime() - 1000)) + .split(" ")[0]; + + const doc = frappe.model.get_new_doc("Leave Application"); + doc.from_date = from_date; + doc.to_date = to_date; + doc.employee = + (await hrms.get_current_employee()) || + frappe.defaults.get_user_default("employee"); + + const can_change_employee = frappe.user.has_role([ + "Administrator", + "System Manager", + "HR Manager", + "HR User", + ]); + + frappe.ui.form.make_quick_entry( + "Leave Application", + () => { + if (typeof cur_list !== "undefined" && cur_list.refresh) { + cur_list.refresh(); + } else if (typeof cur_view !== "undefined" && cur_view.refresh) { + cur_view.refresh(); + } + }, + (quick_entry) => { + set_hidden_defaults(quick_entry); + setup_full_form_action(quick_entry); + setup_date_fields_row(quick_entry); + quick_entry.set_df_property("leave_type", "description", ""); + ensure_total_leave_days_display(quick_entry); + ensure_reason_field(quick_entry); + + quick_entry.insert = function () { + return new Promise((resolve, reject) => { + sync_reason_value(quick_entry); + + quick_entry.update_doc(); + const employee = get_quick_entry_employee(quick_entry); + + ensure_leave_approver(quick_entry, employee, { + clear_existing: true, + enforce_mandatory: true, + }) + .then(() => { + quick_entry.update_doc(); + + frappe.call({ + method: "frappe.client.save", + args: { doc: quick_entry.dialog.doc }, + callback: function (r) { + if (!r.exc) { + quick_entry.process_after_insert(r); + resolve(quick_entry.dialog.doc); + } else { + reject(); + } + }, + error: function () { + reject(); + }, + always: function () { + quick_entry.dialog.working = false; + }, + }); + }) + .catch(() => { + quick_entry.dialog.working = false; + reject(); + }); + }); + }; + + HIDDEN_QUICK_ENTRY_FIELDS.forEach((fieldname) => { + hide_field_completely(quick_entry, fieldname); + }); + + set_allowed_leave_types_query(quick_entry, []); + + quick_entry.set_query("employee", () => ({ + query: "erpnext.controllers.queries.employee_query", + })); + + quick_entry.set_df_property( + "employee", + "read_only", + !can_change_employee ? 1 : 0, + ); + + const refresh_for_current_state = () => { + refresh_leave_metrics(quick_entry); + }; + + bind_input_change(quick_entry, "from_date", () => { + refresh_allowed_leave_types(quick_entry).then(() => { + refresh_for_current_state(); + }); + }); + bind_input_change(quick_entry, "to_date", refresh_for_current_state); + bind_link_change(quick_entry, "leave_type", refresh_for_current_state); + bind_link_change(quick_entry, "employee", () => { + const employee = get_quick_entry_employee(quick_entry); + Promise.all([ + sync_leave_approver(quick_entry, employee, { clear_existing: true }), + refresh_allowed_leave_types(quick_entry), + ]).then(() => { + refresh_for_current_state(); + }); + }); + + Promise.all([ + sync_leave_approver(quick_entry, doc.employee), + refresh_allowed_leave_types(quick_entry), + ]).then(() => { + refresh_for_current_state(); + }); + }, + doc, + true, + ); + }, }, get_events_method: "hrms.hr.doctype.leave_application.leave_application.get_events", }; From a2219c79bc7829c9d74ba1fd6644c6220a2c05b8 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Fri, 10 Apr 2026 14:26:51 +0530 Subject: [PATCH 2/4] feat: enhance leave type filtering and improve leave approver sync logic and disabled dialog on week and day view. --- .../leave_application_calendar.js | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/hrms/hr/doctype/leave_application/leave_application_calendar.js b/hrms/hr/doctype/leave_application/leave_application_calendar.js index 0c41f0b260..5b3ec3afac 100644 --- a/hrms/hr/doctype/leave_application/leave_application_calendar.js +++ b/hrms/hr/doctype/leave_application/leave_application_calendar.js @@ -233,12 +233,16 @@ function set_allowed_leave_types_query(quick_entry, allowed_leave_types) { quick_entry._allowed_leave_types = allowed; quick_entry.set_query("leave_type", () => { - if (!quick_entry._allowed_leave_types?.length) { - return {}; - } - return { - filters: [["leave_type_name", "in", quick_entry._allowed_leave_types]], + filters: [ + [ + "leave_type_name", + "in", + quick_entry._allowed_leave_types?.length + ? quick_entry._allowed_leave_types + : ["__no_allowed_leave_type__"], + ], + ], }; }); } @@ -292,7 +296,7 @@ function refresh_allowed_leave_types(quick_entry) { } function format_metric(value) { - return Number(value || 0).toFixed(1); + return `${value ?? 0}`; } function refresh_leave_metrics(quick_entry) { @@ -448,6 +452,9 @@ function setup_full_form_action(quick_entry) { function sync_leave_approver(quick_entry, employee, options = {}) { const { clear_existing = false } = options; + const request_id = (quick_entry._leaveApproverRequestId || 0) + 1; + quick_entry._leaveApproverRequestId = request_id; + const is_latest_request = () => quick_entry._leaveApproverRequestId === request_id; if (clear_existing) { set_leave_approver_value(quick_entry, ""); @@ -461,9 +468,17 @@ function sync_leave_approver(quick_entry, employee, options = {}) { }, }) .then((mandatory_response) => { + if (!is_latest_request()) { + return; + } + const is_mandatory = Number(mandatory_response?.message); if (!employee) { + if (!is_latest_request()) { + return; + } + set_leave_approver_value(quick_entry, ""); return { is_mandatory, leave_approver: "" }; } @@ -476,6 +491,10 @@ function sync_leave_approver(quick_entry, employee, options = {}) { }, }) .then((r) => { + if (!is_latest_request()) { + return; + } + const leave_approver = set_leave_approver_value(quick_entry, r?.message || ""); return { is_mandatory, leave_approver }; }); @@ -528,10 +547,13 @@ frappe.views.calendar["Leave Application"] = { center: "title", right: "month", }, + dateClick: function (info) { + info.jsEvent?.preventDefault?.(); + info.jsEvent?.stopPropagation?.(); + return false; + }, select: async function (info) { - const milliseconds = info.end - info.start; - - if (info.view.type === "dayGridMonth" && milliseconds === 86400000) { + if (info.view.type !== "dayGridMonth") { return; } From 9800fca412ca2c221d96ab746eb4e8c3acb866a9 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Mon, 13 Apr 2026 18:44:57 +0530 Subject: [PATCH 3/4] feat: add leave metrics retrieval and summary display in calendar view --- .../leave_application/leave_application.py | 33 ++++++++ .../leave_application_calendar.js | 84 ++++++------------- .../leave_application_calendar_summary.html | 20 +++++ 3 files changed, 79 insertions(+), 58 deletions(-) create mode 100644 hrms/hr/doctype/leave_application/leave_application_calendar_summary.html diff --git a/hrms/hr/doctype/leave_application/leave_application.py b/hrms/hr/doctype/leave_application/leave_application.py index ba5ff5885b..3b7de73a7f 100755 --- a/hrms/hr/doctype/leave_application/leave_application.py +++ b/hrms/hr/doctype/leave_application/leave_application.py @@ -938,6 +938,28 @@ def get_allocation_expiry_for_cf_leaves( return expiry[0][0] if expiry else "" +@frappe.whitelist() +def get_leave_metrics_and_details( + employee: str, + leave_type: str, + from_date: datetime.date, + to_date: datetime.date, + half_day: int | str | None = None, + half_day_date: datetime.date | str | None = None, +) -> dict: + """Returns both number of leave days and leave allocation details in a single call""" + number_of_leave_days = get_number_of_leave_days( + employee, leave_type, from_date, to_date, half_day, half_day_date + ) + + details = get_leave_details(employee, from_date) + + return { + "number_of_leave_days": number_of_leave_days, + "leave_allocation": details["leave_allocation"], + } + + @frappe.whitelist() def get_number_of_leave_days( employee: str, @@ -1519,3 +1541,14 @@ def get_leave_approver(employee: str) -> str: def on_doctype_update(): frappe.db.add_index("Leave Application", ["employee", "from_date", "to_date"]) + + +@frappe.whitelist() +def get_leave_approver_and_mandatory(employee: str) -> dict: + """Returns both leave approver and mandatory approval status in a single call""" + mandatory = frappe.db.get_single_value("HR Settings", "leave_approver_mandatory_in_leave_application") + + return { + "is_mandatory": 1 if mandatory else 0, + "leave_approver": get_leave_approver(employee), + } diff --git a/hrms/hr/doctype/leave_application/leave_application_calendar.js b/hrms/hr/doctype/leave_application/leave_application_calendar.js index 5b3ec3afac..3e205fd474 100644 --- a/hrms/hr/doctype/leave_application/leave_application_calendar.js +++ b/hrms/hr/doctype/leave_application/leave_application_calendar.js @@ -178,28 +178,11 @@ function set_leave_type_summary(quick_entry, summary) { quick_entry._leave_type_summary .html( - ` -
-
-
- ${__("Allocated")} - ${allocated} -
-
-
-
- ${__("Used")} - ${used} -
-
-
-
- ${__("Remaining")} - ${remaining} -
-
-
- `, + frappe.render_template("leave_application_calendar_summary", { + allocated, + used, + remaining, + }), ) .show(); } @@ -316,36 +299,28 @@ function refresh_leave_metrics(quick_entry) { const request_id = (quick_entry._leave_metrics_request_id || 0) + 1; quick_entry._leave_metrics_request_id = request_id; - Promise.all([ - frappe.call({ - method: "hrms.hr.doctype.leave_application.leave_application.get_number_of_leave_days", + frappe + .call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_metrics_and_details", args: { employee, leave_type, from_date, to_date, }, - }), - frappe.call({ - method: "hrms.hr.doctype.leave_application.leave_application.get_leave_details", - args: { - employee, - date: from_date, - }, - }), - ]) - .then(([days_response, details_response]) => { + }) + .then((response) => { if (quick_entry._leave_metrics_request_id !== request_id) { return; } - const requested_days = days_response?.message; - const allocation = details_response?.message?.leave_allocation?.[leave_type] || {}; + const requested_days = response?.message?.number_of_leave_days; + const allocation = response?.message?.leave_allocation?.[leave_type] || {}; const allocated = format_metric(allocation?.total_leaves); const used = format_metric(allocation?.leaves_taken); const remaining = format_metric(allocation?.remaining_leaves); const has_allocation = Object.prototype.hasOwnProperty.call( - details_response?.message?.leave_allocation || {}, + response?.message?.leave_allocation || {}, leave_type, ); @@ -462,17 +437,17 @@ function sync_leave_approver(quick_entry, employee, options = {}) { return frappe .call({ - method: "hrms.hr.doctype.leave_application.leave_application.get_mandatory_approval", + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_approver_and_mandatory", args: { - doctype: "Leave Application", + employee, }, }) - .then((mandatory_response) => { + .then((response) => { if (!is_latest_request()) { return; } - const is_mandatory = Number(mandatory_response?.message); + const { is_mandatory, leave_approver } = response?.message || {}; if (!employee) { if (!is_latest_request()) { @@ -480,24 +455,17 @@ function sync_leave_approver(quick_entry, employee, options = {}) { } set_leave_approver_value(quick_entry, ""); - return { is_mandatory, leave_approver: "" }; + return { is_mandatory: Number(is_mandatory) || 0, leave_approver: "" }; } - return frappe - .call({ - method: "hrms.hr.doctype.leave_application.leave_application.get_leave_approver", - args: { - employee, - }, - }) - .then((r) => { - if (!is_latest_request()) { - return; - } - - const leave_approver = set_leave_approver_value(quick_entry, r?.message || ""); - return { is_mandatory, leave_approver }; - }); + const leave_approver_value = set_leave_approver_value( + quick_entry, + leave_approver || "", + ); + return { + is_mandatory: Number(is_mandatory) || 0, + leave_approver: leave_approver_value, + }; }) .then((result) => { if (result) { diff --git a/hrms/hr/doctype/leave_application/leave_application_calendar_summary.html b/hrms/hr/doctype/leave_application/leave_application_calendar_summary.html new file mode 100644 index 0000000000..1866251599 --- /dev/null +++ b/hrms/hr/doctype/leave_application/leave_application_calendar_summary.html @@ -0,0 +1,20 @@ +
+
+
+ {{ __("Allocated") }} + {{ allocated }} +
+
+
+
+ {{ __("Used") }} + {{ used }} +
+
+
+
+ {{ __("Remaining") }} + {{ remaining }} +
+
+
\ No newline at end of file From b9192988cc7fc4458e1370074e43a301f55b9178 Mon Sep 17 00:00:00 2001 From: Krishna Shirsath Date: Fri, 17 Apr 2026 14:07:52 +0530 Subject: [PATCH 4/4] feat: add permission check for employee access in leave metrics and approver retrieval --- hrms/hr/doctype/leave_application/leave_application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/leave_application/leave_application.py b/hrms/hr/doctype/leave_application/leave_application.py index 3b7de73a7f..f79d6a1d48 100755 --- a/hrms/hr/doctype/leave_application/leave_application.py +++ b/hrms/hr/doctype/leave_application/leave_application.py @@ -947,7 +947,7 @@ def get_leave_metrics_and_details( half_day: int | str | None = None, half_day_date: datetime.date | str | None = None, ) -> dict: - """Returns both number of leave days and leave allocation details in a single call""" + frappe.has_permission("Employee", "read", employee, throw=True) number_of_leave_days = get_number_of_leave_days( employee, leave_type, from_date, to_date, half_day, half_day_date ) @@ -1545,7 +1545,7 @@ def on_doctype_update(): @frappe.whitelist() def get_leave_approver_and_mandatory(employee: str) -> dict: - """Returns both leave approver and mandatory approval status in a single call""" + frappe.has_permission("Employee", "read", employee, throw=True) mandatory = frappe.db.get_single_value("HR Settings", "leave_approver_mandatory_in_leave_application") return {