diff --git a/time_capture/custom_fields.py b/time_capture/custom_fields.py index fc5c4c5..95d4ccc 100644 --- a/time_capture/custom_fields.py +++ b/time_capture/custom_fields.py @@ -40,6 +40,16 @@ def get_custom_fields(): "reqd": 1, }, ], + "Leave Application": [ + { + "fieldname": "bulk_leave_application", + "fieldtype": "Link", + "insert_after": "letter_head", + "label": _("Bulk Leave Application"), + "options": "Bulk Leave Application", + "read_only": 1, + }, + ], "Task": [ { "fieldname": "custom_hourly_billed", diff --git a/time_capture/hooks.py b/time_capture/hooks.py index e0849be..5a77a58 100644 --- a/time_capture/hooks.py +++ b/time_capture/hooks.py @@ -139,7 +139,7 @@ doc_events = { "Attendance": { - "before_insert": "time_capture.scripts.attendance.before_insert", + "on_change": "time_capture.scripts.attendance.on_change", "on_submit": "time_capture.scripts.attendance.on_submit", "on_cancel": "time_capture.scripts.attendance.on_cancel", }, diff --git a/time_capture/patches.txt b/time_capture/patches.txt index 30a5c90..916a412 100644 --- a/time_capture/patches.txt +++ b/time_capture/patches.txt @@ -3,4 +3,4 @@ # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations [post_model_sync] -execute:from time_capture.install import after_install;after_install() # 2025-03-19 #2 \ No newline at end of file +execute:from time_capture.install import after_install;after_install() # 2025-05-12 \ No newline at end of file diff --git a/time_capture/scripts/attendance.py b/time_capture/scripts/attendance.py index 805eb67..e378c60 100644 --- a/time_capture/scripts/attendance.py +++ b/time_capture/scripts/attendance.py @@ -4,8 +4,9 @@ from time_capture.time_capture.doctype.time_capture.time_capture import _create_time_capture -def before_insert(doc, event): - set_flexitime_for_compensatory_leave(doc) +def on_change(doc, event): + if not doc.flags.flexitime_updated: + set_flexitime_and_working_time(doc) def on_submit(doc, event): @@ -18,10 +19,29 @@ def on_cancel(doc, event): _create_time_capture(employee, doc.attendance_date) -def set_flexitime_for_compensatory_leave(doc): - if not doc.leave_type or frappe.db.get_value("Leave Type", doc.leave_type, "is_compensatory") != 1: - return - doc.flexitime = -frappe.db.get_value("Employee", doc.employee, "expected_daily_working_hours") +def set_flexitime_and_working_time(doc): + doc.flags.flexitime_updated = True + expected_working_hours = frappe.db.get_value("Employee", doc.employee, "expected_daily_working_hours") + working_hours = doc.working_hours + if not doc.leave_type: + if expected_working_hours: + HALF_DAY = expected_working_hours / 2 + OVERTIME_FACTOR = 1.15 + MAX_HALF_DAY = HALF_DAY * OVERTIME_FACTOR * 60 * 60 + doc.db_set({"status": "Present" if working_hours > MAX_HALF_DAY else "Half Day"}) + else: + if frappe.db.get_value("Leave Type", doc.leave_type, "is_compensatory") == 1: + working_hours = 0 + else: + expected_working_hours = 0 + working_hours = 0 + doc.db_set( + { + "expected_working_hours": expected_working_hours, + "working_hours": working_hours, + "flexitime": working_hours - expected_working_hours, + } + ) def delete_time_capture(doc): diff --git a/time_capture/time_capture/doctype/bulk_leave_application/__init__.py b/time_capture/time_capture/doctype/bulk_leave_application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.js b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.js new file mode 100644 index 0000000..472869a --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.js @@ -0,0 +1,57 @@ +// Copyright (c) 2025, ALYF GmbH and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Bulk Leave Application", { +// refresh(frm) { + +// }, +// }); + +frappe.ui.form.on("Bulk Leave Application", { + setup: function (frm) { + frm.set_query("leave_approver", function () { + return { + query: "hrms.hr.doctype.department_approver.department_approver.get_approvers", + filters: { + employee: frm.doc.employee, + doctype: "Leave Application", + }, + }; + }); + frm.set_query("employee", erpnext.queries.employee); + }, + + from_date: function (frm) { + if (frm.doc.from_date && !frm.doc.to_date) { + var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); + frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1)); + } + }, + + employee: function (frm) { + frm.trigger("set_leave_approver"); + }, + + set_leave_approver: function (frm) { + if (frm.doc.employee) { + return frappe.call({ + method: "hrms.hr.doctype.leave_application.leave_application.get_leave_approver", + args: { + employee: frm.doc.employee, + }, + callback: function (r) { + if (r && r.message) { + frm.set_value("leave_approver", r.message); + } + }, + }); + } + }, +}); + + frappe.ui.form.on("Bulk Leave Application Date", { + from_date: function (frm, cdt, cdn) { + frappe.model.set_value(cdt, cdn, "to_date", locals[cdt][cdn].from_date); + }, +}); + diff --git a/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.json b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.json new file mode 100644 index 0000000..d59aedf --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.json @@ -0,0 +1,256 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-04-09 14:17:35.501172", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "column_break_ihkc", + "leave_type", + "from_date", + "to_date", + "add_weekly_holidays", + "weekly_off", + "get_weekly_off_dates", + "dates_section", + "table_leaves", + "approval_section", + "leave_approver", + "column_break_lfyt", + "status", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name" + }, + { + "fieldname": "column_break_ihkc", + "fieldtype": "Column Break" + }, + { + "fieldname": "leave_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Leave Type", + "options": "Leave Type", + "reqd": 1 + }, + { + "fieldname": "approval_section", + "fieldtype": "Section Break", + "label": "Approval" + }, + { + "fieldname": "leave_approver", + "fieldtype": "Link", + "label": "Leave Approver", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "column_break_lfyt", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "status", + "fieldtype": "Select", + "in_filter": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Open\nApproved\nRejected\nCancelled", + "permlevel": 1 + }, + { + "fieldname": "dates_section", + "fieldtype": "Section Break", + "label": "Dates" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Bulk Leave Application", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "table_leaves", + "fieldtype": "Table", + "label": "Leaves", + "options": "Bulk Leave Application Date" + }, + { + "depends_on": "eval: doc.from_date && doc.to_date", + "fieldname": "add_weekly_holidays", + "fieldtype": "Section Break", + "label": "Add Weekly Holidays" + }, + { + "fieldname": "weekly_off", + "fieldtype": "Select", + "label": "Weekly Off", + "options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday" + }, + { + "fieldname": "get_weekly_off_dates", + "fieldtype": "Button", + "label": "Add to Holidays", + "options": "get_weekly_off_dates" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "reqd": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2025-05-12 08:50:33.467370", + "modified_by": "Administrator", + "module": "Time Capture", + "name": "Bulk Leave Application", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Leave Approver", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Leave Approver", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "employee_name" +} \ No newline at end of file diff --git a/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.py b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.py new file mode 100644 index 0000000..15c3044 --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application.py @@ -0,0 +1,72 @@ +# Copyright (c) 2025, ALYF GmbH and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate +from frappe.utils.data import today + +from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange + +from hrms.hr.utils import share_doc_with_approver, get_holiday_dates_for_employee + + +class BulkLeaveApplication(Document): + def on_update(self): + share_doc_with_approver(self, self.leave_approver) + + def on_submit(self): + if self.status in ["Open", "Cancelled"]: + frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")) + if self.status == "Approved": + self.create_attendances() + + def before_cancel(self): + self.status = "Cancelled" + + @frappe.whitelist() + def get_weekly_off_dates(self): + if not self.weekly_off: + frappe.throw(_("Please select weekly off day")) + for d in self.get_weekly_off_date_list(self.from_date, self.to_date): + self.append("table_leaves", {"reason": _(self.weekly_off), "from_date": d, "to_date": d}) + + def get_weekly_off_date_list(self, start_date, end_date): + start_date, end_date = getdate(start_date), getdate(end_date) + + import calendar + from datetime import timedelta + + from dateutil import relativedelta + + date_list = [] + existing_date_list = [] + weekday = getattr(calendar, (self.weekly_off).upper()) + reference_date = start_date + relativedelta.relativedelta(weekday=weekday) + + existing_date_list = [getdate(row.from_date) for row in self.get("table_leaves")] + + while reference_date <= end_date: + if reference_date not in existing_date_list: + date_list.append(reference_date) + reference_date += timedelta(days=7) + + holidays = get_holiday_dates_for_employee(self.employee, start_date, end_date) + date_list = [d for d in date_list if d not in holidays] + return date_list + + def create_attendances(self): + holidays = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date) + for period in self.table_leaves: # TODO: Rename this! + for dt in daterange(getdate(period.from_date), getdate(period.to_date)): + if dt in holidays: + continue + attendance = frappe.new_doc("Attendance") + attendance.employee = self.employee + attendance.employee_name = self.employee_name + attendance.attendance_date = str(dt) + attendance.status = "On Leave" + attendance.leave_type = self.leave_type + attendance.insert() + attendance.submit() diff --git a/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application_list.js b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application_list.js new file mode 100644 index 0000000..6344517 --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application/bulk_leave_application_list.js @@ -0,0 +1,16 @@ +frappe.listview_settings["Bulk Leave Application"] = { + has_indicator_for_draft: 1, + get_indicator: function (doc) { + const status_color = { + Approved: "green", + Rejected: "red", + Open: "orange", + Draft: "red", + Cancelled: "red", + Submitted: "blue", + }; + const status = + !doc.docstatus && ["Approved", "Rejected"].includes(doc.status) ? "Draft" : doc.status; + return [__(status), status_color[status], "status,=," + doc.status]; + }, +}; diff --git a/time_capture/time_capture/doctype/bulk_leave_application/test_bulk_leave_application.py b/time_capture/time_capture/doctype/bulk_leave_application/test_bulk_leave_application.py new file mode 100644 index 0000000..3a702aa --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application/test_bulk_leave_application.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, ALYF GmbH and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBulkLeaveApplication(FrappeTestCase): + pass diff --git a/time_capture/time_capture/doctype/bulk_leave_application_date/__init__.py b/time_capture/time_capture/doctype/bulk_leave_application_date/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/time_capture/time_capture/doctype/bulk_leave_application_date/bulk_leave_application_date.json b/time_capture/time_capture/doctype/bulk_leave_application_date/bulk_leave_application_date.json new file mode 100644 index 0000000..c0ab3e7 --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application_date/bulk_leave_application_date.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-04-09 15:37:31.771057", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from_date", + "to_date", + "reason" + ], + "fields": [ + { + "fieldname": "reason", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Reason" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "From Date", + "reqd": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "To Date", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-05-12 08:31:36.000904", + "modified_by": "Administrator", + "module": "Time Capture", + "name": "Bulk Leave Application Date", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/time_capture/time_capture/doctype/bulk_leave_application_date/bulk_leave_application_date.py b/time_capture/time_capture/doctype/bulk_leave_application_date/bulk_leave_application_date.py new file mode 100644 index 0000000..0651c99 --- /dev/null +++ b/time_capture/time_capture/doctype/bulk_leave_application_date/bulk_leave_application_date.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, ALYF GmbH and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BulkLeaveApplicationDate(Document): + pass diff --git a/time_capture/time_capture/doctype/time_capture/time_capture.py b/time_capture/time_capture/doctype/time_capture/time_capture.py index 47d59f2..47a5f3b 100644 --- a/time_capture/time_capture/doctype/time_capture/time_capture.py +++ b/time_capture/time_capture/doctype/time_capture/time_capture.py @@ -146,24 +146,14 @@ def create_attendance(self): }, ): working_hours = self.working_time / 60 / 60 - expected_working_hours = frappe.get_value( - "Employee", self.employee, "expected_daily_working_hours" - ) - if expected_working_hours: - HALF_DAY = expected_working_hours / 2 - OVERTIME_FACTOR = 1.15 - MAX_HALF_DAY = HALF_DAY * OVERTIME_FACTOR * 60 * 60 attendance = frappe.get_doc( { "doctype": "Attendance", "employee": self.employee, - "status": "Present" if self.working_time > MAX_HALF_DAY else "Half Day", "attendance_date": self.date, "custom_time_capture": self.name, "working_hours": working_hours, - "expected_working_hours": expected_working_hours, - "flexitime": working_hours - expected_working_hours, } ) attendance.flags.ignore_permissions = True