From 888e7aee527267381b31adf1f98ec8af18c1b11a Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Wed, 6 May 2026 16:05:40 +0000 Subject: [PATCH 01/12] feat: v2 comment assign and file upload apis --- .../mobile/v2/commen/__init__.py | 272 ++++++++++++++++++ .../mobile/v2/commen/utils.py | 3 + .../mobile/v2/utils/__init__.py | 90 ++++++ 3 files changed, 365 insertions(+) create mode 100644 employee_self_service/mobile/v2/commen/__init__.py create mode 100644 employee_self_service/mobile/v2/commen/utils.py create mode 100644 employee_self_service/mobile/v2/utils/__init__.py diff --git a/employee_self_service/mobile/v2/commen/__init__.py b/employee_self_service/mobile/v2/commen/__init__.py new file mode 100644 index 0000000..5bc0a73 --- /dev/null +++ b/employee_self_service/mobile/v2/commen/__init__.py @@ -0,0 +1,272 @@ +from employee_self_service.mobile.v2.utils import * +from frappe.desk.form import assign_to +from frappe.handler import upload_file +import frappe.model as frappe_model + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def assign_document( + doctype, + docname, + users, + description=None +): + try: + if not frappe.db.exists(doctype, docname): + return gen_response(404, "Document not found") + + assign_to.add( + dict( + assign_to=users, + doctype=doctype, + name=docname, + description=description or "Assigned From Mobile App" + ) + ) + return gen_response( + 200, + "Assignment completed successfully" + ) + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_assignments(doctype, docname): + try: + assignments = frappe.get_all( + "ToDo", + filters={ + "reference_type": doctype, + "reference_name": docname, + "status": ["!=", "Cancelled"] + }, + fields=[ + "name", + "allocated_to", + "description", + "status", + "date" + ] + ) + + users = list(set([ + d.allocated_to for d in assignments if d.allocated_to + ])) + user_details = frappe.get_all( + "User", + filters={ + "name": ["in", users] + }, + fields=[ + "name", + "full_name", + "user_image" + ] + ) + + user_map = { + user.name: user for user in user_details + } + + for row in assignments: + user = user_map.get(row.allocated_to, {}) + row["full_name"] = user.get("full_name") + row["user_image"] = user.get("user_image") + + return gen_response( + 200, + "Assignments fetched successfully", + { + "total_assignments": len(assignments), + "assignments": assignments + } + ) + + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def remove_assignment( + doctype, + docname, + user +): + try: + assign_to.remove( + doctype=doctype, + name=docname, + assign_to=user + ) + return gen_response( + 200, + "Assignment removed successfully" + ) + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def clear_assignments( + doctype, + docname +): + try: + assign_to.clear( + doctype, + docname + ) + return gen_response( + 200, + "All assignments cleared successfully" + ) + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def upload_documents(): + try: + if not frappe.form_dict.reference_doctype: + return gen_response(500, "Please provide a reference document type.") + + if not frappe.form_dict.reference_docname: + return gen_response(500, "Please provide a reference document name.") + + if "file" in frappe.request.files: + file_doc = upload_file() + file_doc.attached_to_doctype = frappe.form_dict.reference_doctype + file_doc.attached_to_name = frappe.form_dict.reference_docname + + is_private = frappe.form_dict.get("is_private", "1") + file_doc.is_private = int(is_private) + file_doc.save() + + return gen_response(200, "File uploaded successfully.", { + "name": file_doc.name, + "file_url": file_doc.file_url, + "file_name": file_doc.file_name, + "is_private": file_doc.is_private, + }) + else: + return gen_response(500, "Please upload a file for attachment.") + + except frappe.PermissionError: + return gen_response(403, "Not permitted to upload this file.") + except Exception as e: + frappe.db.rollback() + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def delete_file(file_name): + try: + if not file_name: + return gen_response(500, "Please provide the file name.") + + if not frappe.db.exists("File", file_name): + return gen_response(404, "File not found.") + + frappe.delete_doc("File", file_name) + + return gen_response(200, "File deleted successfully.") + + except frappe.PermissionError: + return gen_response(403, "Not permitted to delete this file.") + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_attachments(reference_doctype, reference_name): + try: + if not reference_doctype: + return gen_response(500, "Please provide a reference document type.") + + if not reference_name: + return gen_response(500, "Please provide a reference document name.") + + files = frappe.get_all( + "File", + filters={ + "attached_to_doctype": reference_doctype, + "attached_to_name": reference_name, + }, + fields=["name", "file_name", "file_url", "is_private", "file_size"], + ) + + return gen_response(200, "Attachments fetched successfully.", files) + + except frappe.PermissionError: + return gen_response(403, "Not permitted to view attachments.") + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_option_list(doctype,field_name): + try: + meta = frappe.get_meta(doctype) + status_field_meta = meta.get_field(field_name) + status_options = [] + + if status_field_meta and status_field_meta.options: + status_options = [ + opt.strip() + for opt in status_field_meta.options.split("\n") + if opt.strip() + ] + + return gen_response( + 200, + "Options fetched successfully", + status_options, + ) + + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_list_sort_options(doctype): + try: + if doctype.startswith("tab"): + doctype = doctype[3:] + + meta = frappe.get_meta(doctype) + + SORTABLE_FIELDTYPES = { + "Data", "Link", "Select", "Date", "Datetime", + "Int", "Float", "Currency", "Check", + } + + sort_fields = [ + {"fieldname": "name", "label": "ID"}, + {"fieldname": "modified", "label": "Last Updated On"}, + {"fieldname": "creation", "label": "Created On"}, + ] + added = {"name", "modified", "creation"} + + for df in meta.fields: + if ( + df.fieldname + and df.label + and df.fieldtype in SORTABLE_FIELDTYPES + and df.fieldname not in added + ): + sort_fields.append({"fieldname": df.fieldname, "label": df.label}) + added.add(df.fieldname) + + return gen_response( + 200, + "Sort options fetched successfully", + sort_fields, + ) + + except Exception as e: + return exception_handler(e) + diff --git a/employee_self_service/mobile/v2/commen/utils.py b/employee_self_service/mobile/v2/commen/utils.py new file mode 100644 index 0000000..340266b --- /dev/null +++ b/employee_self_service/mobile/v2/commen/utils.py @@ -0,0 +1,3 @@ +import frappe +from frappe import _ +from employee_self_service.employee_self_service.mobile.v2.utils import * \ No newline at end of file diff --git a/employee_self_service/mobile/v2/utils/__init__.py b/employee_self_service/mobile/v2/utils/__init__.py new file mode 100644 index 0000000..15a7157 --- /dev/null +++ b/employee_self_service/mobile/v2/utils/__init__.py @@ -0,0 +1,90 @@ +import frappe +import wrapt +from bs4 import BeautifulSoup +from frappe import _ + +def gen_response(status, message, data=[]): + frappe.response["http_status_code"] = status + if status == 500: + frappe.response["message"] = BeautifulSoup(str(message)).get_text() + else: + frappe.response["message"] = message + frappe.response["data"] = data + + +def exception_handler(e): + frappe.log_error(title="ESS Mobile App Error", message=frappe.get_traceback()) + if hasattr(e, "http_status_code"): + return gen_response(e.http_status_code, BeautifulSoup(str(e)).get_text()) + else: + return gen_response(500, BeautifulSoup(str(e)).get_text()) + + +def generate_key(user): + user_details = frappe.get_doc("User", user) + api_secret = api_key = "" + if not user_details.api_key and not user_details.api_secret: + api_secret = frappe.generate_hash(length=15) + # if api key is not set generate api key + api_key = frappe.generate_hash(length=15) + user_details.api_key = api_key + user_details.api_secret = api_secret + user_details.save(ignore_permissions=True) + else: + api_secret = user_details.get_password("api_secret") + api_key = user_details.get("api_key") + return {"api_secret": api_secret, "api_key": api_key} + + +def ess_validate(methods): + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + if frappe.local.request.method not in methods: + return gen_response(500, "Invalid Request Method") + return wrapped(*args, **kwargs) + + return wrapper + + +def get_employee_by_user(user, fields=["name"]): + if isinstance(fields, str): + fields = [fields] + emp_data = frappe.get_cached_value( + "Employee", + {"user_id": user}, + fields, + as_dict=1, + ) + return emp_data + + +def get_ess_settings(): + return frappe.get_doc( + "Employee Self Service Settings", "Employee Self Service Settings" + ) + + +def get_global_defaults(): + return frappe.get_doc("Global Defaults", "Global Defaults") + + +def remove_default_fields(data): + # Example usage: + # remove_default_fields( + # json.loads( + # frappe.get_doc("Address", "name").as_json() + # ) + # ) + for row in [ + "owner", + "creation", + "modified", + "modified_by", + "docstatus", + "idx", + "doctype", + "links", + ]: + if data.get(row): + del data[row] + return data \ No newline at end of file From d78e919908080e51e549bf4d5bec20d3be56a255 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Wed, 6 May 2026 18:49:10 +0000 Subject: [PATCH 02/12] feat: further leave,profile and v2 apis related changes --- .../constants/custom_fields.py | 8 + .../employee_self_service_settings.json | 39 ++ .../employee_update_request/__init__.py | 0 .../employee_update_request.js | 145 +++++ .../employee_update_request.json | 118 ++++ .../employee_update_request.py | 60 ++ .../test_employee_update_request.py | 9 + employee_self_service/mobile/v1/ess.py | 140 +++++ .../mobile/v2/commen/__init__.py | 532 +++++++++--------- .../mobile/v2/commen/utils.py | 4 +- .../mobile/v2/utils/__init__.py | 178 +++--- 11 files changed, 870 insertions(+), 363 deletions(-) create mode 100644 employee_self_service/employee_self_service/doctype/employee_update_request/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js create mode 100644 employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json create mode 100644 employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py create mode 100644 employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py diff --git a/employee_self_service/constants/custom_fields.py b/employee_self_service/constants/custom_fields.py index 8480958..1146915 100644 --- a/employee_self_service/constants/custom_fields.py +++ b/employee_self_service/constants/custom_fields.py @@ -77,6 +77,14 @@ "insert_after": "disabled", }, ], + "Leave Application": [ + { + "fieldname": "medical_supporting_document", + "label": "Medical Supporting Document", + "fieldtype": "Attach", + "insert_after": "follow_via_email", + }, + ], } diff --git a/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json b/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json index 5baf580..fbd9764 100755 --- a/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json +++ b/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json @@ -21,6 +21,12 @@ "allow_user_to_change_uom", "column_break_kxpj", "visit_proof_required", + "hr_settings_section", + "required_medical_document", + "column_break_orrh", + "medical_leave_type", + "column_break_gpvb", + "allow_edit_profile", "section_break_xjas", "enable_ess_notification", "check_in_with_image", @@ -292,6 +298,39 @@ "fieldname": "allow_user_to_change_uom", "fieldtype": "Check", "label": "Allow User to Change UOM" + }, + { + "fieldname": "hr_settings_section", + "fieldtype": "Section Break", + "label": "HR Settings" + }, + { + "default": "0", + "fieldname": "required_medical_document", + "fieldtype": "Check", + "label": "Required Medical Document" + }, + { + "fieldname": "column_break_orrh", + "fieldtype": "Column Break" + }, + { + "description": "Select the leave type used for medical reasons when applying for leave.", + "fieldname": "medical_leave_type", + "fieldtype": "Link", + "label": "Medical Leave Type", + "mandatory_depends_on": "required_medical_document", + "options": "Leave Type" + }, + { + "fieldname": "column_break_gpvb", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "allow_edit_profile", + "fieldtype": "Check", + "label": "Allow Edit Profile" } ], "grid_page_length": 50, diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/__init__.py b/employee_self_service/employee_self_service/doctype/employee_update_request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js b/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js new file mode 100644 index 0000000..ad7f43b --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js @@ -0,0 +1,145 @@ +// Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Employee Update Request", { + refresh(frm) { + render_diff(frm); + }, +}); + +const FIELD_LABELS = { + first_name: __("Full Name"), + gender: __("Gender"), + date_of_birth: __("Date of Birth"), + date_of_joining: __("Date of Joining"), + designation: __("Designation"), + cell_number: __("Cell Number"), + personal_email: __("Personal Email"), + current_address: __("Current Address"), + emergency_phone_number: __("Emergency Phone Number"), + marital_status: __("Marital Status"), + blood_group: __("Blood Group"), +}; + +function render_diff(frm) { + const wrapper = frm.fields_dict.table_html.$wrapper; + + if (!frm.doc.data) { + wrapper.empty(); + return; + } + + let data; + + try { + data = JSON.parse(frm.doc.data); + } catch { + wrapper.empty(); + return; + } + + const old_values = data.old || {}; + const new_values = data.new || {}; + + let rows = ""; + + Object.entries(new_values).forEach(([field, value]) => { + + if (field === "education") return; + + rows += ` + + ${FIELD_LABELS[field] || field} + + + ${frappe.utils.escape_html(String(old_values[field] || "—"))} + + + + ${frappe.utils.escape_html(String(value || "—"))} + + + `; + }); + + if (new_values.education?.length) { + + const render_education_table = (list = []) => { + + if (!list.length) { + return `
`; + } + + return ` + + + + + + + + + + + + ${list.map(row => ` + + + + + + + `).join("")} + +
${__("School/University")}${__("Qualification")}${__("Level")}${__("Year")}
${frappe.utils.escape_html(row.school_univ || "")}${frappe.utils.escape_html(row.qualification || "")}${frappe.utils.escape_html(row.level || "")}${frappe.utils.escape_html(row.year_of_passing || "")}
+ `; + }; + + rows += ` + + ${__("Education")} + + + ${render_education_table(old_values.education)} + + + + ${render_education_table(new_values.education)} + + + `; + } + + if (!rows) { + wrapper.html(` +
+ ${__("No changes requested.")} +
+ `); + return; + } + + wrapper.html(` + + + + + + + + + + + + + ${rows} + +
+ ${__("Field")} + + ${__("Old Value")} + + ${__("New Value")} +
+ `); +} \ No newline at end of file diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json b/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json new file mode 100644 index 0000000..0d82852 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json @@ -0,0 +1,118 @@ +{ + "actions": [], + "autoname": "EUR.#####", + "creation": "2026-05-06 19:17:52.072698", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "column_break_header", + "requested_on", + "section_changes", + "table_html", + "is_applied", + "data" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "column_break_header", + "fieldtype": "Column Break" + }, + { + "default": "Now", + "fieldname": "requested_on", + "fieldtype": "Datetime", + "label": "Requested On", + "read_only": 1 + }, + { + "fieldname": "section_changes", + "fieldtype": "Section Break", + "label": "Values Changed" + }, + { + "fieldname": "table_html", + "fieldtype": "HTML", + "label": "Change Summary" + }, + { + "default": "0", + "fieldname": "is_applied", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Applied" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "hidden": 1, + "label": "Change Data (JSON)" + } + ], + "icon": "fa fa-user-edit", + "in_create": 1, + "links": [], + "modified": "2026-05-06 23:20:43.779662", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "Employee Update Request", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "export": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Administrator", + "write": 1 + }, + { + "create": 1, + "read": 1, + "role": "Employee" + }, + { + "read": 1, + "role": "HR User", + "write": 1 + }, + { + "read": 1, + "role": "HR Manager", + "write": 1 + } + ], + "row_format": "Compressed", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "employee_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py b/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py new file mode 100644 index 0000000..c63c54b --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +import json +import frappe +from frappe.model.document import Document + +EMPLOYEE_FIELDS = { + "first_name": "Full Name", + "gender": "Gender", + "date_of_birth": "Date of Birth", + "date_of_joining": "Date of Joining", + "designation": "Designation", + "cell_number": "Cell Number", + "personal_email": "Personal Email", + "current_address": "Current Address", + "emergency_phone_number": "Emergency Phone Number", + "marital_status": "Marital Status", + "blood_group": "Blood Group", +} + + +class EmployeeUpdateRequest(Document): + def on_update(self): + if self.workflow_state != "Approved" or self.is_applied: + return + + self.apply_to_employee() + frappe.db.set_value( + self.doctype, + self.name, + "is_applied", + 1, + ) + + def apply_to_employee(self): + if not self.data: + return + + data = json.loads(self.data) + new_values = data.get("new", {}) + education = new_values.pop("education", None) + + emp_doc = frappe.get_doc("Employee", self.employee) + + for field, value in new_values.items(): + if field in emp_doc.meta.get_valid_columns(): + emp_doc.set(field, value or None) + + if education is not None: + emp_doc.set("education", []) + + for row in education: + emp_doc.append("education", { + "school_univ": row.get("school_univ"), + "qualification": row.get("qualification"), + "level": row.get("level"), + "year_of_passing": row.get("year_of_passing"), + }) + emp_doc.save() \ No newline at end of file diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py b/employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py new file mode 100644 index 0000000..b05c0dc --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestEmployeeUpdateRequest(FrappeTestCase): + pass diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index c797ac0..b65fa93 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -1,5 +1,6 @@ import calendar import os +import json import frappe from erpnext.accounts.utils import get_fiscal_year @@ -194,6 +195,11 @@ def make_leave_application(*args, **kwargs): if not len(emp_data) >= 1: return gen_response(500, "Employee does not exists!") validate_employee_data(emp_data) + + setting = get_ess_settings() + if setting.required_medical_document and setting.medical_leave_type == kwargs.get("leave_type") and not kwargs.get("medical_supporting_document"): + return gen_response(500, "Medical document is required for this leave type!") + leave_application_doc = frappe.get_doc( doctype="Leave Application", employee=emp_data.get("name"), @@ -284,6 +290,18 @@ def get_leave_type(from_date=None, to_date=None): except Exception as e: return exception_handler(e) +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def medical_document_required_check(leave_type): + try: + setting = get_ess_settings() + if setting.required_medical_document and setting.medical_leave_type == leave_type: + return gen_response(200, "Medical document is required for this leave type!", {"medical_document_required": True}) + + return gen_response(200, "Medical document Required Flag get successfully", {"medical_document_required": False}) + + except Exception as e: + return exception_handler(e) @frappe.whitelist() @ess_validate(methods=["GET"]) @@ -1334,12 +1352,134 @@ def get_profile(): employee_details["employee_image"] = frappe.get_cached_value( "Employee", emp_data.get("name"), "image" ) + setting = get_ess_settings() + employee_details["allow_edit_profile"] = bool( + setting.allow_edit_profile + ) + + employee_details["has_pending_edit_request"] = bool(frappe.db.exists( + "Employee Update Request", + { + "employee": emp_data.get("name"), + "workflow_state": "Pending", + } + )) return gen_response(200, "Profile get successfully", employee_details) except Exception as e: return exception_handler(e) +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def update_profile(**kwargs): + try: + employee = get_employee_by_user(frappe.session.user) + validate_employee_data(employee) + + employee_name = employee.get("name") + + if frappe.db.exists( + "Employee Update Request", + { + "employee": employee_name, + "workflow_state": "Pending", + }, + ): + return gen_response( + 400, + "You already have a pending profile update request." + ) + + field_map = { + "new_first_name": "first_name", + "gender": "gender", + "date_of_birth": "date_of_birth", + "date_of_joining": "date_of_joining", + "designation": "designation", + "cell_number": "cell_number", + "personal_email": "personal_email", + "current_address": "current_address", + "emergency_phone_number": "emergency_phone_number", + "marital_status": "marital_status", + "blood_group": "blood_group", + } + + emp_doc = frappe.get_doc("Employee", employee_name) + + old_data = {} + new_data = {} + + for request_key, employee_field in field_map.items(): + + new_value = kwargs.get(request_key) + + if new_value in [None, ""]: + continue + + old_value = emp_doc.get(employee_field) + + if str(old_value or "") != str(new_value): + old_data[employee_field] = old_value or "" + new_data[employee_field] = new_value + + education = kwargs.get("education", []) + if education: + old_education = frappe.get_all( + "Employee Education", + filters={"parent": employee_name}, + fields=[ + "school_univ", + "qualification", + "level", + "year_of_passing", + ], + order_by="idx asc", + ) + + new_education = [ + row for row in education if row.get("school_univ") + ] + + if old_education != new_education: + old_data["education"] = old_education + new_data["education"] = new_education + + if not new_data: + return gen_response( + 400, + "No changes found." + ) + + request_doc = frappe.get_doc({ + "doctype": "Employee Update Request", + "employee": employee_name, + "data": json.dumps( + { + "old": old_data, + "new": new_data, + }, + default=str, + ), + }) + request_doc.insert() + + return gen_response( + 200, + "Profile update request submitted successfully.", + { + "name": request_doc.name + }, + ) + + except frappe.PermissionError: + return gen_response( + 403, + "Not permitted to update profile." + ) + except Exception as e: + return exception_handler(e) + @frappe.whitelist() @ess_validate(methods=["POST"]) def upload_documents(): diff --git a/employee_self_service/mobile/v2/commen/__init__.py b/employee_self_service/mobile/v2/commen/__init__.py index 5bc0a73..1c2dd86 100644 --- a/employee_self_service/mobile/v2/commen/__init__.py +++ b/employee_self_service/mobile/v2/commen/__init__.py @@ -1,272 +1,260 @@ -from employee_self_service.mobile.v2.utils import * -from frappe.desk.form import assign_to -from frappe.handler import upload_file -import frappe.model as frappe_model - -@frappe.whitelist() -@ess_validate(methods=["POST"]) -def assign_document( - doctype, - docname, - users, - description=None -): - try: - if not frappe.db.exists(doctype, docname): - return gen_response(404, "Document not found") - - assign_to.add( - dict( - assign_to=users, - doctype=doctype, - name=docname, - description=description or "Assigned From Mobile App" - ) - ) - return gen_response( - 200, - "Assignment completed successfully" - ) - except Exception as e: - return exception_handler(e) - -@frappe.whitelist() -@ess_validate(methods=["GET"]) -def get_assignments(doctype, docname): - try: - assignments = frappe.get_all( - "ToDo", - filters={ - "reference_type": doctype, - "reference_name": docname, - "status": ["!=", "Cancelled"] - }, - fields=[ - "name", - "allocated_to", - "description", - "status", - "date" - ] - ) - - users = list(set([ - d.allocated_to for d in assignments if d.allocated_to - ])) - user_details = frappe.get_all( - "User", - filters={ - "name": ["in", users] - }, - fields=[ - "name", - "full_name", - "user_image" - ] - ) - - user_map = { - user.name: user for user in user_details - } - - for row in assignments: - user = user_map.get(row.allocated_to, {}) - row["full_name"] = user.get("full_name") - row["user_image"] = user.get("user_image") - - return gen_response( - 200, - "Assignments fetched successfully", - { - "total_assignments": len(assignments), - "assignments": assignments - } - ) - - except Exception as e: - return exception_handler(e) - -@frappe.whitelist() -@ess_validate(methods=["POST"]) -def remove_assignment( - doctype, - docname, - user -): - try: - assign_to.remove( - doctype=doctype, - name=docname, - assign_to=user - ) - return gen_response( - 200, - "Assignment removed successfully" - ) - except Exception as e: - return exception_handler(e) - -@frappe.whitelist() -@ess_validate(methods=["POST"]) -def clear_assignments( - doctype, - docname -): - try: - assign_to.clear( - doctype, - docname - ) - return gen_response( - 200, - "All assignments cleared successfully" - ) - except Exception as e: - return exception_handler(e) - - -@frappe.whitelist() -@ess_validate(methods=["POST"]) -def upload_documents(): - try: - if not frappe.form_dict.reference_doctype: - return gen_response(500, "Please provide a reference document type.") - - if not frappe.form_dict.reference_docname: - return gen_response(500, "Please provide a reference document name.") - - if "file" in frappe.request.files: - file_doc = upload_file() - file_doc.attached_to_doctype = frappe.form_dict.reference_doctype - file_doc.attached_to_name = frappe.form_dict.reference_docname - - is_private = frappe.form_dict.get("is_private", "1") - file_doc.is_private = int(is_private) - file_doc.save() - - return gen_response(200, "File uploaded successfully.", { - "name": file_doc.name, - "file_url": file_doc.file_url, - "file_name": file_doc.file_name, - "is_private": file_doc.is_private, - }) - else: - return gen_response(500, "Please upload a file for attachment.") - - except frappe.PermissionError: - return gen_response(403, "Not permitted to upload this file.") - except Exception as e: - frappe.db.rollback() - return exception_handler(e) - -@frappe.whitelist() -@ess_validate(methods=["POST"]) -def delete_file(file_name): - try: - if not file_name: - return gen_response(500, "Please provide the file name.") - - if not frappe.db.exists("File", file_name): - return gen_response(404, "File not found.") - - frappe.delete_doc("File", file_name) - - return gen_response(200, "File deleted successfully.") - - except frappe.PermissionError: - return gen_response(403, "Not permitted to delete this file.") - except Exception as e: - return exception_handler(e) - - -@frappe.whitelist() -@ess_validate(methods=["GET"]) -def get_attachments(reference_doctype, reference_name): - try: - if not reference_doctype: - return gen_response(500, "Please provide a reference document type.") - - if not reference_name: - return gen_response(500, "Please provide a reference document name.") - - files = frappe.get_all( - "File", - filters={ - "attached_to_doctype": reference_doctype, - "attached_to_name": reference_name, - }, - fields=["name", "file_name", "file_url", "is_private", "file_size"], - ) - - return gen_response(200, "Attachments fetched successfully.", files) - - except frappe.PermissionError: - return gen_response(403, "Not permitted to view attachments.") - except Exception as e: - return exception_handler(e) - -@frappe.whitelist() -@ess_validate(methods=["GET"]) -def get_option_list(doctype,field_name): - try: - meta = frappe.get_meta(doctype) - status_field_meta = meta.get_field(field_name) - status_options = [] - - if status_field_meta and status_field_meta.options: - status_options = [ - opt.strip() - for opt in status_field_meta.options.split("\n") - if opt.strip() - ] - - return gen_response( - 200, - "Options fetched successfully", - status_options, - ) - - except Exception as e: - return exception_handler(e) - - -@frappe.whitelist() -@ess_validate(methods=["GET"]) -def get_list_sort_options(doctype): - try: - if doctype.startswith("tab"): - doctype = doctype[3:] - - meta = frappe.get_meta(doctype) - - SORTABLE_FIELDTYPES = { - "Data", "Link", "Select", "Date", "Datetime", - "Int", "Float", "Currency", "Check", - } - - sort_fields = [ - {"fieldname": "name", "label": "ID"}, - {"fieldname": "modified", "label": "Last Updated On"}, - {"fieldname": "creation", "label": "Created On"}, - ] - added = {"name", "modified", "creation"} - - for df in meta.fields: - if ( - df.fieldname - and df.label - and df.fieldtype in SORTABLE_FIELDTYPES - and df.fieldname not in added - ): - sort_fields.append({"fieldname": df.fieldname, "label": df.label}) - added.add(df.fieldname) - - return gen_response( - 200, - "Sort options fetched successfully", - sort_fields, - ) - - except Exception as e: - return exception_handler(e) - +from employee_self_service.mobile.v2.utils import * +from frappe.desk.form import assign_to +from frappe.handler import upload_file +import frappe.model as frappe_model + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def assign_document( + doctype, + docname, + users, + description=None +): + try: + if not frappe.db.exists(doctype, docname): + return gen_response(404, "Document not found") + + assign_to.add( + dict( + assign_to=users, + doctype=doctype, + name=docname, + description=description or "Assigned From Mobile App" + ) + ) + return gen_response( + 200, + "Assignment completed successfully" + ) + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_assignments(doctype, docname): + try: + assignments = frappe.get_all( + "ToDo", + filters={ + "reference_type": doctype, + "reference_name": docname, + "status": ["!=", "Cancelled"] + }, + fields=[ + "name", + "allocated_to", + "description", + "status", + "date" + ] + ) + + users = list(set([ + d.allocated_to for d in assignments if d.allocated_to + ])) + user_details = frappe.get_all( + "User", + filters={ + "name": ["in", users] + }, + fields=[ + "name", + "full_name", + "user_image" + ] + ) + + user_map = { + user.name: user for user in user_details + } + + for row in assignments: + user = user_map.get(row.allocated_to, {}) + row["full_name"] = user.get("full_name") + row["user_image"] = user.get("user_image") + + return gen_response( + 200, + "Assignments fetched successfully", + { + "total_assignments": len(assignments), + "assignments": assignments + } + ) + + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def remove_assignment( + doctype, + docname, + user +): + try: + assign_to.remove( + doctype=doctype, + name=docname, + assign_to=user + ) + return gen_response( + 200, + "Assignment removed successfully" + ) + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def clear_assignments( + doctype, + docname +): + try: + assign_to.clear( + doctype, + docname + ) + return gen_response( + 200, + "All assignments cleared successfully" + ) + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def upload_documents(): + try: + if not frappe.form_dict.reference_doctype: + return gen_response(500, "Please provide a reference document type.") + + if not frappe.form_dict.reference_docname: + return gen_response(500, "Please provide a reference document name.") + + if "file" in frappe.request.files: + file_doc = upload_file() + file_doc.attached_to_doctype = frappe.form_dict.reference_doctype + file_doc.attached_to_name = frappe.form_dict.reference_docname + + is_private = frappe.form_dict.get("is_private", "1") + file_doc.is_private = int(is_private) + file_doc.save() + + return gen_response(200, "File uploaded successfully.", { + "name": file_doc.name, + "file_url": file_doc.file_url, + "file_name": file_doc.file_name, + "is_private": file_doc.is_private, + }) + else: + return gen_response(500, "Please upload a file for attachment.") + + except frappe.PermissionError: + return gen_response(403, "Not permitted to upload this file.") + except Exception as e: + frappe.db.rollback() + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def delete_file(file_name): + try: + if not file_name: + return gen_response(500, "Please provide the file name.") + + if not frappe.db.exists("File", file_name): + return gen_response(404, "File not found.") + + frappe.delete_doc("File", file_name) + + return gen_response(200, "File deleted successfully.") + + except frappe.PermissionError: + return gen_response(403, "Not permitted to delete this file.") + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_attachments(reference_doctype, reference_name): + try: + if not reference_doctype: + return gen_response(500, "Please provide a reference document type.") + + if not reference_name: + return gen_response(500, "Please provide a reference document name.") + + files = frappe.get_all( + "File", + filters={ + "attached_to_doctype": reference_doctype, + "attached_to_name": reference_name, + }, + fields=["name", "file_name", "file_url", "is_private", "file_size"], + ) + + return gen_response(200, "Attachments fetched successfully.", files) + + except frappe.PermissionError: + return gen_response(403, "Not permitted to view attachments.") + except Exception as e: + return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_option_list(doctype,field_name): + try: + meta = frappe.get_meta(doctype) + status_field_meta = meta.get_field(field_name) + status_options = [] + + if status_field_meta and status_field_meta.options: + status_options = [ + opt.strip() + for opt in status_field_meta.options.split("\n") + if opt.strip() + ] + + return gen_response( + 200, + "Options fetched successfully", + status_options, + ) + + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_sort_option_list(doctype): + try: + meta = frappe.get_meta(doctype) + + options = [ + {"fieldname": "modified", "label": "Last Updated On"}, + {"fieldname": "name", "label": "ID"}, + {"fieldname": "creation", "label": "Created On"}, + ] + + layout_fieldtypes = { + "Section Break", "Column Break", "Tab Break", + "HTML", "Table", "Table MultiSelect", + "Button", "Image", "Fold", "Heading", + } + + for df in meta.fields: + if df.in_list_view and df.fieldtype not in layout_fieldtypes: + options.append({"fieldname": df.fieldname, "label": df.label}) + + return gen_response(200, "Sort options fetched successfully", options) + + except Exception as e: + return exception_handler(e) + + diff --git a/employee_self_service/mobile/v2/commen/utils.py b/employee_self_service/mobile/v2/commen/utils.py index 340266b..3738805 100644 --- a/employee_self_service/mobile/v2/commen/utils.py +++ b/employee_self_service/mobile/v2/commen/utils.py @@ -1,3 +1,3 @@ -import frappe -from frappe import _ +import frappe +from frappe import _ from employee_self_service.employee_self_service.mobile.v2.utils import * \ No newline at end of file diff --git a/employee_self_service/mobile/v2/utils/__init__.py b/employee_self_service/mobile/v2/utils/__init__.py index 15a7157..fe3b1d1 100644 --- a/employee_self_service/mobile/v2/utils/__init__.py +++ b/employee_self_service/mobile/v2/utils/__init__.py @@ -1,90 +1,90 @@ -import frappe -import wrapt -from bs4 import BeautifulSoup -from frappe import _ - -def gen_response(status, message, data=[]): - frappe.response["http_status_code"] = status - if status == 500: - frappe.response["message"] = BeautifulSoup(str(message)).get_text() - else: - frappe.response["message"] = message - frappe.response["data"] = data - - -def exception_handler(e): - frappe.log_error(title="ESS Mobile App Error", message=frappe.get_traceback()) - if hasattr(e, "http_status_code"): - return gen_response(e.http_status_code, BeautifulSoup(str(e)).get_text()) - else: - return gen_response(500, BeautifulSoup(str(e)).get_text()) - - -def generate_key(user): - user_details = frappe.get_doc("User", user) - api_secret = api_key = "" - if not user_details.api_key and not user_details.api_secret: - api_secret = frappe.generate_hash(length=15) - # if api key is not set generate api key - api_key = frappe.generate_hash(length=15) - user_details.api_key = api_key - user_details.api_secret = api_secret - user_details.save(ignore_permissions=True) - else: - api_secret = user_details.get_password("api_secret") - api_key = user_details.get("api_key") - return {"api_secret": api_secret, "api_key": api_key} - - -def ess_validate(methods): - @wrapt.decorator - def wrapper(wrapped, instance, args, kwargs): - if frappe.local.request.method not in methods: - return gen_response(500, "Invalid Request Method") - return wrapped(*args, **kwargs) - - return wrapper - - -def get_employee_by_user(user, fields=["name"]): - if isinstance(fields, str): - fields = [fields] - emp_data = frappe.get_cached_value( - "Employee", - {"user_id": user}, - fields, - as_dict=1, - ) - return emp_data - - -def get_ess_settings(): - return frappe.get_doc( - "Employee Self Service Settings", "Employee Self Service Settings" - ) - - -def get_global_defaults(): - return frappe.get_doc("Global Defaults", "Global Defaults") - - -def remove_default_fields(data): - # Example usage: - # remove_default_fields( - # json.loads( - # frappe.get_doc("Address", "name").as_json() - # ) - # ) - for row in [ - "owner", - "creation", - "modified", - "modified_by", - "docstatus", - "idx", - "doctype", - "links", - ]: - if data.get(row): - del data[row] +import frappe +import wrapt +from bs4 import BeautifulSoup +from frappe import _ + +def gen_response(status, message, data=[]): + frappe.response["http_status_code"] = status + if status == 500: + frappe.response["message"] = BeautifulSoup(str(message)).get_text() + else: + frappe.response["message"] = message + frappe.response["data"] = data + + +def exception_handler(e): + frappe.log_error(title="ESS Mobile App Error", message=frappe.get_traceback()) + if hasattr(e, "http_status_code"): + return gen_response(e.http_status_code, BeautifulSoup(str(e)).get_text()) + else: + return gen_response(500, BeautifulSoup(str(e)).get_text()) + + +def generate_key(user): + user_details = frappe.get_doc("User", user) + api_secret = api_key = "" + if not user_details.api_key and not user_details.api_secret: + api_secret = frappe.generate_hash(length=15) + # if api key is not set generate api key + api_key = frappe.generate_hash(length=15) + user_details.api_key = api_key + user_details.api_secret = api_secret + user_details.save(ignore_permissions=True) + else: + api_secret = user_details.get_password("api_secret") + api_key = user_details.get("api_key") + return {"api_secret": api_secret, "api_key": api_key} + + +def ess_validate(methods): + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + if frappe.local.request.method not in methods: + return gen_response(500, "Invalid Request Method") + return wrapped(*args, **kwargs) + + return wrapper + + +def get_employee_by_user(user, fields=["name"]): + if isinstance(fields, str): + fields = [fields] + emp_data = frappe.get_cached_value( + "Employee", + {"user_id": user}, + fields, + as_dict=1, + ) + return emp_data + + +def get_ess_settings(): + return frappe.get_doc( + "Employee Self Service Settings", "Employee Self Service Settings" + ) + + +def get_global_defaults(): + return frappe.get_doc("Global Defaults", "Global Defaults") + + +def remove_default_fields(data): + # Example usage: + # remove_default_fields( + # json.loads( + # frappe.get_doc("Address", "name").as_json() + # ) + # ) + for row in [ + "owner", + "creation", + "modified", + "modified_by", + "docstatus", + "idx", + "doctype", + "links", + ]: + if data.get(row): + del data[row] return data \ No newline at end of file From 85ce88f4c951f2231bf2526e0cc61faa16f052a4 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Wed, 6 May 2026 20:05:53 +0000 Subject: [PATCH 03/12] feat: employee profile details update workflow added --- employee_self_service/fixtures/workflow.json | 93 ++++++++++++++++++++ employee_self_service/hooks.py | 1 + 2 files changed, 94 insertions(+) create mode 100644 employee_self_service/fixtures/workflow.json diff --git a/employee_self_service/fixtures/workflow.json b/employee_self_service/fixtures/workflow.json new file mode 100644 index 0000000..2385723 --- /dev/null +++ b/employee_self_service/fixtures/workflow.json @@ -0,0 +1,93 @@ +[ + { + "docstatus": 0, + "doctype": "Workflow", + "document_type": "Employee Update Request", + "is_active": 1, + "modified": "2026-05-06 23:00:17.731565", + "name": "Employee Details Update Workflow", + "override_status": 0, + "send_email_alert": 0, + "states": [ + { + "allow_edit": "HR Manager", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Employee Details Update Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Pending", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + }, + { + "allow_edit": "HR Manager", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Employee Details Update Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Approved", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + }, + { + "allow_edit": "HR Manager", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Employee Details Update Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Rejected", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + } + ], + "transitions": [ + { + "action": "Approve", + "allow_self_approval": 1, + "allowed": "HR Manager", + "condition": null, + "next_state": "Approved", + "parent": "Employee Details Update Workflow", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending", + "workflow_builder_id": null + }, + { + "action": "Reject", + "allow_self_approval": 1, + "allowed": "HR Manager", + "condition": null, + "next_state": "Rejected", + "parent": "Employee Details Update Workflow", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending", + "workflow_builder_id": null + } + ], + "workflow_data": null, + "workflow_name": "Employee Details Update Workflow", + "workflow_state_field": "workflow_state" + } +] \ No newline at end of file diff --git a/employee_self_service/hooks.py b/employee_self_service/hooks.py index 0f66cb2..9004495 100644 --- a/employee_self_service/hooks.py +++ b/employee_self_service/hooks.py @@ -219,4 +219,5 @@ # }, {"dt": "ESS Notification"}, {"dt": "ESS Notification Template"}, + {"dt": "Workflow", "filters": [["document_type", "in", ["Employee Update Request"]]]}, ] From 644be60eb197d09c3168d0912a5f44431a106b34 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Thu, 7 May 2026 10:51:31 +0000 Subject: [PATCH 04/12] feat: include visit document name in response for visit creation and update --- employee_self_service/mobile/v1/ess.py | 2 +- employee_self_service/mobile/v1/visit.py | 4 ++-- employee_self_service/mobile/v2/commen/__init__.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index b65fa93..d0bfd39 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -208,7 +208,7 @@ def make_leave_application(*args, **kwargs): ) leave_application_doc.update(kwargs) leave_application_doc.insert() - gen_response(200, "Leave application successfully added!") + gen_response(200, "Leave application successfully added!", leave_application_doc.name) except Exception as e: return exception_handler(e) diff --git a/employee_self_service/mobile/v1/visit.py b/employee_self_service/mobile/v1/visit.py index 4a8c28c..b159a0f 100644 --- a/employee_self_service/mobile/v1/visit.py +++ b/employee_self_service/mobile/v1/visit.py @@ -39,7 +39,7 @@ def create_visit(**kwargs): visit_doc.employee = emp_data.get("name") visit_doc.update(data) visit_doc.save(ignore_permissions=True) - return gen_response(200, "Visit updated Successfully") + return gen_response(200, "Visit updated Successfully", visit_doc.name) else: visit_doc = frappe.new_doc("Visit") if not frappe.db.exists("Customer", data.get("customer")): @@ -54,7 +54,7 @@ def create_visit(**kwargs): visit_doc.employee = emp_data.get("name") visit_doc.update(data) visit_doc.insert() - return gen_response(200, "Visit created Successfully") + return gen_response(200, "Visit created Successfully", visit_doc.name) except frappe.PermissionError: return gen_response(500, "Not permitted create visit") except Exception as e: diff --git a/employee_self_service/mobile/v2/commen/__init__.py b/employee_self_service/mobile/v2/commen/__init__.py index 1c2dd86..c37f12c 100644 --- a/employee_self_service/mobile/v2/commen/__init__.py +++ b/employee_self_service/mobile/v2/commen/__init__.py @@ -1,7 +1,6 @@ from employee_self_service.mobile.v2.utils import * from frappe.desk.form import assign_to from frappe.handler import upload_file -import frappe.model as frappe_model @frappe.whitelist() @ess_validate(methods=["POST"]) From 7ced2af083807fc92c89710a61562a71893d1890 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Thu, 7 May 2026 13:13:05 +0000 Subject: [PATCH 05/12] feat: leave application api rleated changes --- employee_self_service/mobile/ess.py | 1 + employee_self_service/mobile/v1/ess.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/employee_self_service/mobile/ess.py b/employee_self_service/mobile/ess.py index 4e095e9..0d2645e 100644 --- a/employee_self_service/mobile/ess.py +++ b/employee_self_service/mobile/ess.py @@ -37,6 +37,7 @@ from employee_self_service.utils import get_employees_having_an_event_today + @frappe.whitelist(allow_guest=True) def login(usr, pwd): try: diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index d0bfd39..9abdb0a 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -196,10 +196,6 @@ def make_leave_application(*args, **kwargs): return gen_response(500, "Employee does not exists!") validate_employee_data(emp_data) - setting = get_ess_settings() - if setting.required_medical_document and setting.medical_leave_type == kwargs.get("leave_type") and not kwargs.get("medical_supporting_document"): - return gen_response(500, "Medical document is required for this leave type!") - leave_application_doc = frappe.get_doc( doctype="Leave Application", employee=emp_data.get("name"), @@ -377,6 +373,7 @@ def get_leave_application(name): "to_date", "posting_date", "half_day_date", + "medical_supporting_document" ] leave_application = frappe.db.get_value( From 313c6717082c58257ab3e252cfcba5a553be6766 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Thu, 7 May 2026 14:13:04 +0000 Subject: [PATCH 06/12] feat: leave application api related changes --- employee_self_service/mobile/v1/ess.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index 9abdb0a..27a702f 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -372,14 +372,26 @@ def get_leave_application(name): "from_date", "to_date", "posting_date", - "half_day_date", - "medical_supporting_document" + "half_day_date" ] leave_application = frappe.db.get_value( "Leave Application", name, leave_application_fields, as_dict=True ) + file_data = frappe.db.get_value( + "File", + { + "attached_to_doctype": "Leave Application", + "attached_to_name": name, + "attached_to_field": "medical_supporting_document", + }, + ["file_name","name","file_url"], + as_dict=True, + ) + + leave_application["medical_supporting_document"] = file_data + return gen_response(200, "Leave data getting successfully", leave_application) except Exception as e: return exception_handler(e) From bb92adfd2acd8b3e153508414d929cc781a3e1ba Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Fri, 8 May 2026 05:39:01 +0000 Subject: [PATCH 07/12] feat: update employee profile details to use cint for boolean values --- employee_self_service/mobile/v1/ess.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index 27a702f..8eb5011 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -17,6 +17,7 @@ cstr, date_diff, flt, + cint, fmt_money, get_date_str, get_first_day, @@ -1362,11 +1363,11 @@ def get_profile(): "Employee", emp_data.get("name"), "image" ) setting = get_ess_settings() - employee_details["allow_edit_profile"] = bool( + employee_details["allow_edit_profile"] = cint( setting.allow_edit_profile ) - employee_details["has_pending_edit_request"] = bool(frappe.db.exists( + employee_details["has_pending_edit_request"] = cint(frappe.db.exists( "Employee Update Request", { "employee": emp_data.get("name"), From 3aa5abd381f54850f3d91cfd3664e30c113ed491 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Wed, 13 May 2026 05:37:38 +0000 Subject: [PATCH 08/12] feat: employee update request doctype rename and further profile api related changes --- .../__init__.py | 0 .../employee_details_update_request.js} | 5 ++- .../employee_details_update_request.json} | 5 ++- .../employee_details_update_request.py} | 18 ++-------- .../test_employee_details_update_request.py} | 2 +- employee_self_service/fixtures/workflow.json | 2 +- employee_self_service/hooks.py | 2 +- employee_self_service/mobile/v1/ess.py | 21 ++++------- .../mobile/v2/commen/__init__.py | 36 +++++++++++++++++++ 9 files changed, 51 insertions(+), 40 deletions(-) rename employee_self_service/employee_self_service/doctype/{employee_update_request => employee_details_update_request}/__init__.py (100%) rename employee_self_service/employee_self_service/doctype/{employee_update_request/employee_update_request.js => employee_details_update_request/employee_details_update_request.js} (97%) rename employee_self_service/employee_self_service/doctype/{employee_update_request/employee_update_request.json => employee_details_update_request/employee_details_update_request.json} (95%) rename employee_self_service/employee_self_service/doctype/{employee_update_request/employee_update_request.py => employee_details_update_request/employee_details_update_request.py} (73%) rename employee_self_service/employee_self_service/doctype/{employee_update_request/test_employee_update_request.py => employee_details_update_request/test_employee_details_update_request.py} (74%) diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/__init__.py b/employee_self_service/employee_self_service/doctype/employee_details_update_request/__init__.py similarity index 100% rename from employee_self_service/employee_self_service/doctype/employee_update_request/__init__.py rename to employee_self_service/employee_self_service/doctype/employee_details_update_request/__init__.py diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.js similarity index 97% rename from employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js rename to employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.js index ad7f43b..e319bc5 100644 --- a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.js +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.js @@ -1,7 +1,7 @@ // Copyright (c) 2026, Nesscale Solutions Private Limited and contributors // For license information, please see license.txt -frappe.ui.form.on("Employee Update Request", { +frappe.ui.form.on("Employee Details Update Request", { refresh(frm) { render_diff(frm); }, @@ -12,8 +12,7 @@ const FIELD_LABELS = { gender: __("Gender"), date_of_birth: __("Date of Birth"), date_of_joining: __("Date of Joining"), - designation: __("Designation"), - cell_number: __("Cell Number"), + cell_number: __("Mobile"), personal_email: __("Personal Email"), current_address: __("Current Address"), emergency_phone_number: __("Emergency Phone Number"), diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json similarity index 95% rename from employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json rename to employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json index 0d82852..0161ffb 100644 --- a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.json +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json @@ -68,12 +68,11 @@ } ], "icon": "fa fa-user-edit", - "in_create": 1, "links": [], - "modified": "2026-05-06 23:20:43.779662", + "modified": "2026-05-12 11:14:09.789517", "modified_by": "Administrator", "module": "Employee Self Service", - "name": "Employee Update Request", + "name": "Employee Details Update Request", "naming_rule": "Expression", "owner": "Administrator", "permissions": [ diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py similarity index 73% rename from employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py rename to employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py index c63c54b..df9145b 100644 --- a/employee_self_service/employee_self_service/doctype/employee_update_request/employee_update_request.py +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py @@ -5,22 +5,8 @@ import frappe from frappe.model.document import Document -EMPLOYEE_FIELDS = { - "first_name": "Full Name", - "gender": "Gender", - "date_of_birth": "Date of Birth", - "date_of_joining": "Date of Joining", - "designation": "Designation", - "cell_number": "Cell Number", - "personal_email": "Personal Email", - "current_address": "Current Address", - "emergency_phone_number": "Emergency Phone Number", - "marital_status": "Marital Status", - "blood_group": "Blood Group", -} - - -class EmployeeUpdateRequest(Document): + +class EmployeeDetailsUpdateRequest(Document): def on_update(self): if self.workflow_state != "Approved" or self.is_applied: return diff --git a/employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py b/employee_self_service/employee_self_service/doctype/employee_details_update_request/test_employee_details_update_request.py similarity index 74% rename from employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py rename to employee_self_service/employee_self_service/doctype/employee_details_update_request/test_employee_details_update_request.py index b05c0dc..c4babca 100644 --- a/employee_self_service/employee_self_service/doctype/employee_update_request/test_employee_update_request.py +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/test_employee_details_update_request.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestEmployeeUpdateRequest(FrappeTestCase): +class TestEmployeeDetailsUpdateRequest(FrappeTestCase): pass diff --git a/employee_self_service/fixtures/workflow.json b/employee_self_service/fixtures/workflow.json index 2385723..f06b175 100644 --- a/employee_self_service/fixtures/workflow.json +++ b/employee_self_service/fixtures/workflow.json @@ -2,7 +2,7 @@ { "docstatus": 0, "doctype": "Workflow", - "document_type": "Employee Update Request", + "document_type": "Employee Details Update Request", "is_active": 1, "modified": "2026-05-06 23:00:17.731565", "name": "Employee Details Update Workflow", diff --git a/employee_self_service/hooks.py b/employee_self_service/hooks.py index 9004495..4b5216d 100644 --- a/employee_self_service/hooks.py +++ b/employee_self_service/hooks.py @@ -219,5 +219,5 @@ # }, {"dt": "ESS Notification"}, {"dt": "ESS Notification Template"}, - {"dt": "Workflow", "filters": [["document_type", "in", ["Employee Update Request"]]]}, + {"dt": "Workflow", "filters": [["document_type", "in", ["Employee Details Update Request"]]]}, ] diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index 8eb5011..cdb245c 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -1367,14 +1367,6 @@ def get_profile(): setting.allow_edit_profile ) - employee_details["has_pending_edit_request"] = cint(frappe.db.exists( - "Employee Update Request", - { - "employee": emp_data.get("name"), - "workflow_state": "Pending", - } - )) - return gen_response(200, "Profile get successfully", employee_details) except Exception as e: return exception_handler(e) @@ -1390,23 +1382,19 @@ def update_profile(**kwargs): employee_name = employee.get("name") if frappe.db.exists( - "Employee Update Request", + "Employee Details Update Request", { "employee": employee_name, "workflow_state": "Pending", }, ): - return gen_response( - 400, - "You already have a pending profile update request." - ) + frappe.throw(_("You already have a pending profile update request.")) field_map = { "new_first_name": "first_name", "gender": "gender", "date_of_birth": "date_of_birth", "date_of_joining": "date_of_joining", - "designation": "designation", "cell_number": "cell_number", "personal_email": "personal_email", "current_address": "current_address", @@ -1462,7 +1450,7 @@ def update_profile(**kwargs): ) request_doc = frappe.get_doc({ - "doctype": "Employee Update Request", + "doctype": "Employee Details Update Request", "employee": employee_name, "data": json.dumps( { @@ -2375,6 +2363,7 @@ def get_profile_detail_tabs(): response = {} personal_details = {} + personal_details["employee_name"] = emp_doc.employee_name personal_details["date_of_birth"] = emp_doc.date_of_birth personal_details["personal_email"] = emp_doc.personal_email personal_details["gender"] = emp_doc.gender @@ -2382,6 +2371,8 @@ def get_profile_detail_tabs(): personal_details["current_address"] = emp_doc.current_address personal_details["person_to_be_contacted"] = emp_doc.person_to_be_contacted personal_details["emergency_phone_number"] = emp_doc.emergency_phone_number + personal_details["marital_status"] = emp_doc.marital_status + personal_details["blood_group"] = emp_doc.blood_group response["personal_details"] = personal_details education_details = {} diff --git a/employee_self_service/mobile/v2/commen/__init__.py b/employee_self_service/mobile/v2/commen/__init__.py index c37f12c..941b319 100644 --- a/employee_self_service/mobile/v2/commen/__init__.py +++ b/employee_self_service/mobile/v2/commen/__init__.py @@ -1,6 +1,7 @@ from employee_self_service.mobile.v2.utils import * from frappe.desk.form import assign_to from frappe.handler import upload_file +from frappe.utils import cint @frappe.whitelist() @ess_validate(methods=["POST"]) @@ -256,4 +257,39 @@ def get_sort_option_list(doctype): except Exception as e: return exception_handler(e) +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_link_option_list( + doctype, + fields=None, + filters=None, + start=0, + page_length=20, +): + try: + if not frappe.db.exists("DocType", doctype): + return gen_response(404, f"DocType '{doctype}' not found") + + link_options = frappe.get_all( + doctype, + fields=fields or ["name"], + filters=filters or [], + start=cint(start), + page_length=cint(page_length), + order_by=f"`tab{doctype}`.modified desc", + ) + + return gen_response( + 200, + "Link options fetched successfully", + link_options, + ) + + except frappe.PermissionError: + return gen_response( + 403, + "Not permitted to access this DocType." + ) + except Exception as e: + return exception_handler(e) From ad5377b43dd1f69cdd8d89a2632411200e439a59 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Wed, 13 May 2026 06:33:42 +0000 Subject: [PATCH 09/12] feat: add date of joining inside get profile details tabs api --- employee_self_service/mobile/v1/ess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index cdb245c..2325c4f 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -2364,7 +2364,8 @@ def get_profile_detail_tabs(): personal_details = {} personal_details["employee_name"] = emp_doc.employee_name - personal_details["date_of_birth"] = emp_doc.date_of_birth + personal_details["date_of_joining"] = emp_doc.date_of_joining.strftime("%d-%m-%Y") if emp_doc.date_of_joining else "" + personal_details["date_of_birth"] = emp_doc.date_of_birth.strftime("%d-%m-%Y") if emp_doc.date_of_birth else "" personal_details["personal_email"] = emp_doc.personal_email personal_details["gender"] = emp_doc.gender personal_details["cell_number"] = emp_doc.cell_number From da57117328bd5b07fc77ab6573d075542c285ea9 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Thu, 14 May 2026 20:24:01 +0000 Subject: [PATCH 10/12] feat: correct commen directory spell and optimized get document upload and assisgn to apis --- .../mobile/v2/commen/utils.py | 3 - .../mobile/v2/{commen => common}/__init__.py | 110 ++++++++++-------- .../mobile/v2/common/utils.py | 2 + 3 files changed, 62 insertions(+), 53 deletions(-) delete mode 100644 employee_self_service/mobile/v2/commen/utils.py rename employee_self_service/mobile/v2/{commen => common}/__init__.py (75%) create mode 100644 employee_self_service/mobile/v2/common/utils.py diff --git a/employee_self_service/mobile/v2/commen/utils.py b/employee_self_service/mobile/v2/commen/utils.py deleted file mode 100644 index 3738805..0000000 --- a/employee_self_service/mobile/v2/commen/utils.py +++ /dev/null @@ -1,3 +0,0 @@ -import frappe -from frappe import _ -from employee_self_service.employee_self_service.mobile.v2.utils import * \ No newline at end of file diff --git a/employee_self_service/mobile/v2/commen/__init__.py b/employee_self_service/mobile/v2/common/__init__.py similarity index 75% rename from employee_self_service/mobile/v2/commen/__init__.py rename to employee_self_service/mobile/v2/common/__init__.py index 941b319..7eea503 100644 --- a/employee_self_service/mobile/v2/commen/__init__.py +++ b/employee_self_service/mobile/v2/common/__init__.py @@ -1,4 +1,10 @@ -from employee_self_service.mobile.v2.utils import * +import frappe +from frappe import _ +from employee_self_service.mobile.v2.utils import ( + gen_response, + exception_handler, + ess_validate, +) from frappe.desk.form import assign_to from frappe.handler import upload_file from frappe.utils import cint @@ -34,45 +40,39 @@ def assign_document( @ess_validate(methods=["GET"]) def get_assignments(doctype, docname): try: - assignments = frappe.get_all( - "ToDo", - filters={ - "reference_type": doctype, - "reference_name": docname, - "status": ["!=", "Cancelled"] - }, - fields=[ - "name", - "allocated_to", - "description", - "status", - "date" - ] - ) + assignments = assign_to.get({ + "doctype": doctype, + "name": docname + }) + + if not assignments: + return gen_response( + 200, + "Assignments fetched successfully", + { + "total_assignments": 0, + "assignments": [] + } + ) + + users = [d.owner for d in assignments if d.owner] - users = list(set([ - d.allocated_to for d in assignments if d.allocated_to - ])) user_details = frappe.get_all( "User", - filters={ - "name": ["in", users] - }, - fields=[ - "name", - "full_name", - "user_image" - ] + filters={"name": ["in", users]}, + fields=["name", "full_name", "user_image"] ) user_map = { - user.name: user for user in user_details + user.name: user + for user in user_details } for row in assignments: - user = user_map.get(row.allocated_to, {}) - row["full_name"] = user.get("full_name") - row["user_image"] = user.get("user_image") + user = user_map.get(row.owner) + + row["full_name"] = user.full_name if user else None + row["user_image"] = user.user_image if user else None return gen_response( 200, @@ -129,34 +129,44 @@ def clear_assignments( @ess_validate(methods=["POST"]) def upload_documents(): try: - if not frappe.form_dict.reference_doctype: - return gen_response(500, "Please provide a reference document type.") - - if not frappe.form_dict.reference_docname: - return gen_response(500, "Please provide a reference document name.") - - if "file" in frappe.request.files: - file_doc = upload_file() - file_doc.attached_to_doctype = frappe.form_dict.reference_doctype - file_doc.attached_to_name = frappe.form_dict.reference_docname + form_dict = frappe.form_dict - is_private = frappe.form_dict.get("is_private", "1") - file_doc.is_private = int(is_private) - file_doc.save() + reference_doctype = form_dict.get("reference_doctype") + reference_docname = form_dict.get("reference_docname") - return gen_response(200, "File uploaded successfully.", { + if not reference_doctype: + return gen_response(400, "Please provide a reference document type.") + + if not reference_docname: + return gen_response(400, "Please provide a reference document name.") + + if "file" not in frappe.request.files: + return gen_response(400, "Please upload a file for attachment.") + + file_doc = upload_file() + + file_doc.update({ + "attached_to_doctype": reference_doctype, + "attached_to_name": reference_docname, + "is_private": cint(form_dict.get("is_private", 1)) + }) + + file_doc.save() + + return gen_response( + 200, + "File uploaded successfully.", + { "name": file_doc.name, "file_url": file_doc.file_url, "file_name": file_doc.file_name, "is_private": file_doc.is_private, - }) - else: - return gen_response(500, "Please upload a file for attachment.") - + } + ) + except frappe.PermissionError: return gen_response(403, "Not permitted to upload this file.") except Exception as e: - frappe.db.rollback() return exception_handler(e) @frappe.whitelist() diff --git a/employee_self_service/mobile/v2/common/utils.py b/employee_self_service/mobile/v2/common/utils.py new file mode 100644 index 0000000..fb89fc6 --- /dev/null +++ b/employee_self_service/mobile/v2/common/utils.py @@ -0,0 +1,2 @@ +import frappe +from frappe import _ \ No newline at end of file From c573ac2e2fcb06165bcfe0c752f08c10bf190510 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Sat, 16 May 2026 10:54:24 +0000 Subject: [PATCH 11/12] feat: employee update details doctype and related api changes --- .../employee_details_update_request.json | 48 +++++++++- .../employee_details_update_request.py | 37 +++++--- .../employee_details_update_request_list.js | 13 +++ employee_self_service/fixtures/workflow.json | 93 ------------------- employee_self_service/hooks.py | 1 - employee_self_service/mobile/v1/ess.py | 2 +- 6 files changed, 83 insertions(+), 111 deletions(-) create mode 100644 employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request_list.js delete mode 100644 employee_self_service/fixtures/workflow.json diff --git a/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json index 0161ffb..a34169e 100644 --- a/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.json @@ -9,11 +9,13 @@ "employee", "employee_name", "column_break_header", + "status", "requested_on", "section_changes", "table_html", "is_applied", - "data" + "data", + "amended_from" ], "fields": [ { @@ -65,11 +67,34 @@ "fieldtype": "Code", "hidden": 1, "label": "Change Data (JSON)" + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Pending\nApproved\nRejected\nCancelled", + "permlevel": 1, + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Details Update Request", + "print_hide": 1, + "read_only": 1, + "search_index": 1 } ], "icon": "fa fa-user-edit", + "is_submittable": 1, "links": [], - "modified": "2026-05-12 11:14:09.789517", + "modified": "2026-05-16 16:03:51.378919", "modified_by": "Administrator", "module": "Employee Self Service", "name": "Employee Details Update Request", @@ -83,6 +108,7 @@ "read": 1, "report": 1, "role": "System Manager", + "submit": 1, "write": 1 }, { @@ -98,11 +124,29 @@ "role": "Employee" }, { + "amend": 1, + "cancel": 1, + "read": 1, + "role": "HR User", + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, "read": 1, "role": "HR User", "write": 1 }, { + "amend": 1, + "cancel": 1, + "read": 1, + "role": "HR Manager", + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, "read": 1, "role": "HR Manager", "write": 1 diff --git a/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py index df9145b..a8a6c6f 100644 --- a/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request.py @@ -3,33 +3,41 @@ import json import frappe +from frappe import _ from frappe.model.document import Document class EmployeeDetailsUpdateRequest(Document): - def on_update(self): - if self.workflow_state != "Approved" or self.is_applied: - return + def on_submit(self): + if self.status in ["Pending", "Cancelled"]: + frappe.throw( + _("Only Employee Details Update Requests with status 'Approved' or 'Rejected' can be submitted") + ) + + if self.status == "Approved" and not self.is_applied: + self.apply_to_employee() + frappe.db.set_value(self.doctype, self.name, "is_applied", 1) - self.apply_to_employee() - frappe.db.set_value( - self.doctype, - self.name, - "is_applied", - 1, - ) + def before_cancel(self): + self.status = "Cancelled" - def apply_to_employee(self): + def on_cancel(self): + if self.is_applied: + self.apply_to_employee(reverse=True) + frappe.db.set_value(self.doctype, self.name, "is_applied", 0) + + def apply_to_employee(self, reverse=False): if not self.data: return data = json.loads(self.data) - new_values = data.get("new", {}) - education = new_values.pop("education", None) + key = "old" if reverse else "new" + values = data.get(key, {}) + education = values.pop("education", None) emp_doc = frappe.get_doc("Employee", self.employee) - for field, value in new_values.items(): + for field, value in values.items(): if field in emp_doc.meta.get_valid_columns(): emp_doc.set(field, value or None) @@ -43,4 +51,5 @@ def apply_to_employee(self): "level": row.get("level"), "year_of_passing": row.get("year_of_passing"), }) + emp_doc.save() \ No newline at end of file diff --git a/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request_list.js b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request_list.js new file mode 100644 index 0000000..b954070 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/employee_details_update_request/employee_details_update_request_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings["Employee Details Update Request"] = { + add_fields: ["status"], + has_indicator_for_draft: 1, + get_indicator: function (doc) { + const status_color = { + Pending: "orange", + Approved: "green", + Rejected: "red", + Cancelled: "red", + }; + return [__(doc.status), status_color[doc.status] || "grey", "status,=," + doc.status]; + }, +}; diff --git a/employee_self_service/fixtures/workflow.json b/employee_self_service/fixtures/workflow.json deleted file mode 100644 index f06b175..0000000 --- a/employee_self_service/fixtures/workflow.json +++ /dev/null @@ -1,93 +0,0 @@ -[ - { - "docstatus": 0, - "doctype": "Workflow", - "document_type": "Employee Details Update Request", - "is_active": 1, - "modified": "2026-05-06 23:00:17.731565", - "name": "Employee Details Update Workflow", - "override_status": 0, - "send_email_alert": 0, - "states": [ - { - "allow_edit": "HR Manager", - "avoid_status_override": 0, - "doc_status": "0", - "is_optional_state": 0, - "message": null, - "next_action_email_template": null, - "parent": "Employee Details Update Workflow", - "parentfield": "states", - "parenttype": "Workflow", - "send_email": 1, - "state": "Pending", - "update_field": null, - "update_value": null, - "workflow_builder_id": null - }, - { - "allow_edit": "HR Manager", - "avoid_status_override": 0, - "doc_status": "0", - "is_optional_state": 0, - "message": null, - "next_action_email_template": null, - "parent": "Employee Details Update Workflow", - "parentfield": "states", - "parenttype": "Workflow", - "send_email": 1, - "state": "Approved", - "update_field": null, - "update_value": null, - "workflow_builder_id": null - }, - { - "allow_edit": "HR Manager", - "avoid_status_override": 0, - "doc_status": "0", - "is_optional_state": 0, - "message": null, - "next_action_email_template": null, - "parent": "Employee Details Update Workflow", - "parentfield": "states", - "parenttype": "Workflow", - "send_email": 1, - "state": "Rejected", - "update_field": null, - "update_value": null, - "workflow_builder_id": null - } - ], - "transitions": [ - { - "action": "Approve", - "allow_self_approval": 1, - "allowed": "HR Manager", - "condition": null, - "next_state": "Approved", - "parent": "Employee Details Update Workflow", - "parentfield": "transitions", - "parenttype": "Workflow", - "send_email_to_creator": 0, - "state": "Pending", - "workflow_builder_id": null - }, - { - "action": "Reject", - "allow_self_approval": 1, - "allowed": "HR Manager", - "condition": null, - "next_state": "Rejected", - "parent": "Employee Details Update Workflow", - "parentfield": "transitions", - "parenttype": "Workflow", - "send_email_to_creator": 0, - "state": "Pending", - "workflow_builder_id": null - } - ], - "workflow_data": null, - "workflow_name": "Employee Details Update Workflow", - "workflow_state_field": "workflow_state" - } -] \ No newline at end of file diff --git a/employee_self_service/hooks.py b/employee_self_service/hooks.py index 4b5216d..0f66cb2 100644 --- a/employee_self_service/hooks.py +++ b/employee_self_service/hooks.py @@ -219,5 +219,4 @@ # }, {"dt": "ESS Notification"}, {"dt": "ESS Notification Template"}, - {"dt": "Workflow", "filters": [["document_type", "in", ["Employee Details Update Request"]]]}, ] diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index 2325c4f..286e050 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -1385,7 +1385,7 @@ def update_profile(**kwargs): "Employee Details Update Request", { "employee": employee_name, - "workflow_state": "Pending", + "status": "Pending", }, ): frappe.throw(_("You already have a pending profile update request.")) From 237639f60616fe70f9d18ea8d146b6c22637bf3c Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Sat, 16 May 2026 11:52:37 +0000 Subject: [PATCH 12/12] feat: bump version to 2.2.4 --- employee_self_service/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/employee_self_service/__init__.py b/employee_self_service/__init__.py index f394e69..62fa04d 100644 --- a/employee_self_service/__init__.py +++ b/employee_self_service/__init__.py @@ -1 +1 @@ -__version__ = "2.2.3" +__version__ = "2.2.4"