diff --git a/hrms/hr/doctype/leave_application/leave_application.py b/hrms/hr/doctype/leave_application/leave_application.py
index 079d98127a..62fc4e88e0 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:
+ 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
+ )
+
+ 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,
@@ -1523,3 +1545,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:
+ frappe.has_permission("Employee", "read", employee, throw=True)
+ 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 6fc85da2e2..3e205fd474 100644
--- a/hrms/hr/doctype/leave_application/leave_application_calendar.js
+++ b/hrms/hr/doctype/leave_application/leave_application_calendar.js
@@ -1,6 +1,505 @@
// 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(
+ frappe.render_template("leave_application_calendar_summary", {
+ allocated,
+ used,
+ 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", () => {
+ return {
+ filters: [
+ [
+ "leave_type_name",
+ "in",
+ quick_entry._allowed_leave_types?.length
+ ? quick_entry._allowed_leave_types
+ : ["__no_allowed_leave_type__"],
+ ],
+ ],
+ };
+ });
+}
+
+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 `${value ?? 0}`;
+}
+
+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;
+
+ frappe
+ .call({
+ method: "hrms.hr.doctype.leave_application.leave_application.get_leave_metrics_and_details",
+ args: {
+ employee,
+ leave_type,
+ from_date,
+ to_date,
+ },
+ })
+ .then((response) => {
+ if (quick_entry._leave_metrics_request_id !== request_id) {
+ return;
+ }
+
+ 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(
+ 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;
+ 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, "");
+ }
+
+ return frappe
+ .call({
+ method: "hrms.hr.doctype.leave_application.leave_application.get_leave_approver_and_mandatory",
+ args: {
+ employee,
+ },
+ })
+ .then((response) => {
+ if (!is_latest_request()) {
+ return;
+ }
+
+ const { is_mandatory, leave_approver } = response?.message || {};
+
+ if (!employee) {
+ if (!is_latest_request()) {
+ return;
+ }
+
+ set_leave_approver_value(quick_entry, "");
+ return { is_mandatory: Number(is_mandatory) || 0, 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) {
+ 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 +515,140 @@ frappe.views.calendar["Leave Application"] = {
center: "title",
right: "month",
},
+ dateClick: function (info) {
+ info.jsEvent?.preventDefault?.();
+ info.jsEvent?.stopPropagation?.();
+ return false;
+ },
+ select: async function (info) {
+ if (info.view.type !== "dayGridMonth") {
+ 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",
};
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