diff --git a/hrms/api/__init__.py b/hrms/api/__init__.py index 7a2e14d015..f20af0a265 100644 --- a/hrms/api/__init__.py +++ b/hrms/api/__init__.py @@ -274,6 +274,8 @@ def get_filters( @frappe.whitelist() def get_shift_request_approvers(employee: str) -> str | list[str]: + frappe.has_permission("Employee", "read", employee, throw=True) + shift_request_approver, department = frappe.get_cached_value( "Employee", employee, @@ -282,6 +284,7 @@ def get_shift_request_approvers(employee: str) -> str | list[str]: department_approvers = [] if department: + frappe.has_permission("Department", "read", department, throw=True) department_approvers = get_department_approvers(department, "shift_request_approver") if not shift_request_approver: shift_request_approver = frappe.db.get_value( @@ -408,6 +411,8 @@ def get_holidays_for_employee(employee: str) -> list[dict]: if not holiday_list: return [] + frappe.has_permission("Holiday List", "read", holiday_list, throw=True) + Holiday = frappe.qb.DocType("Holiday") holidays = ( frappe.qb.from_(Holiday) @@ -424,6 +429,7 @@ def get_holidays_for_employee(employee: str) -> list[dict]: @frappe.whitelist() def get_leave_approval_details(employee: str) -> dict: + frappe.has_permission("Employee", "read", employee, throw=True) leave_approver, department = frappe.get_cached_value( "Employee", employee, @@ -431,6 +437,7 @@ def get_leave_approval_details(employee: str) -> dict: ) if not leave_approver and department: + frappe.has_permission("Department", "read", department, throw=True) leave_approver = frappe.db.get_value( "Department Approver", {"parent": department, "parentfield": "leave_approvers", "idx": 1}, @@ -487,6 +494,7 @@ def get_leave_types(employee: str, date: str) -> list: date = date or getdate() + # Get leave details validate leave access internally leave_details = get_leave_details(employee, date) leave_types = list(leave_details["leave_allocation"].keys()) + leave_details["lwps"] @@ -600,6 +608,7 @@ def get_expense_claim_types() -> list[dict]: @frappe.whitelist() def get_expense_approval_details(employee: str) -> dict: + frappe.has_permission("Employee", "read", employee, throw=True) expense_approver, department = frappe.get_cached_value( "Employee", employee, @@ -607,6 +616,7 @@ def get_expense_approval_details(employee: str) -> dict: ) if not expense_approver and department: + frappe.has_permission("Department", "read", department, throw=True) expense_approver = frappe.db.get_value( "Department Approver", {"parent": department, "parentfield": "expense_approvers", "idx": 1}, @@ -751,6 +761,8 @@ def upload_base64_file( else: file_content = decoded_content + frappe.has_permission(dt, "write", dn, throw=True) + return frappe.get_doc( { "doctype": "File", diff --git a/hrms/api/roster.py b/hrms/api/roster.py index 11719fb16e..bd80a2ad6e 100644 --- a/hrms/api/roster.py +++ b/hrms/api/roster.py @@ -273,6 +273,8 @@ def get_leaves(month_start: str, month_end: str, employee_filters: dict[str, str LeaveApplication = frappe.qb.DocType("Leave Application") Employee = frappe.qb.DocType("Employee") + frappe.has_permission("Leave Application", "read", throw=True) + query = ( frappe.qb.select( LeaveApplication.name.as_("leave"), diff --git a/hrms/hr/doctype/attendance_request/attendance_request.py b/hrms/hr/doctype/attendance_request/attendance_request.py index e0180d5db2..625eb0fd1e 100644 --- a/hrms/hr/doctype/attendance_request/attendance_request.py +++ b/hrms/hr/doctype/attendance_request/attendance_request.py @@ -90,11 +90,13 @@ def validate_shifts(self): ) def get_active_shifts(self): + # Attendance requests are typically posted after the shift period for corrections. + # Expired shift assignments are auto-marked Inactive, but should still be considered + # here so that the shift is auto-fetched for backdated requests. shifts = frappe.get_all( "Shift Assignment", filters={ "docstatus": 1, - "status": "Active", "employee": self.employee, "start_date": ("<=", self.from_date), "end_date": (">=", self.to_date), diff --git a/hrms/hr/doctype/attendance_request/test_attendance_request.py b/hrms/hr/doctype/attendance_request/test_attendance_request.py index 165d15c2a5..4cb8996cf1 100644 --- a/hrms/hr/doctype/attendance_request/test_attendance_request.py +++ b/hrms/hr/doctype/attendance_request/test_attendance_request.py @@ -335,6 +335,44 @@ def test_half_day_with_shift_auto_absent(self): self.assertEqual(attendance.half_day_status, "Absent") self.assertEqual(attendance.modify_half_day_status, 0) + def test_expired_shift_assignment_is_auto_fetched(self): + """Backdated attendance requests should auto-fetch the shift even after the + assignment has been marked Inactive by mark_expired_shift_assignments_as_inactive.""" + from hrms.hr.doctype.shift_assignment.shift_assignment import ( + mark_expired_shift_assignments_as_inactive, + ) + + today = getdate() + shift_start = add_days(today, -10) + shift_end = add_days(today, -5) + + shift_type = create_shift("Test Expired Shift", "09:00:00", "17:00:00") + create_shift_assignment(self.employee.name, shift_type.name, shift_start, shift_end) + + # Cron auto-marks the assignment Inactive because its end_date is in the past + mark_expired_shift_assignments_as_inactive() + + assignment_status = frappe.db.get_value( + "Shift Assignment", + {"employee": self.employee.name, "shift_type": shift_type.name}, + "status", + ) + self.assertEqual(assignment_status, "Inactive") + + # Attendance request for the now-expired shift period — shift should still be auto-fetched + attendance_request = frappe.get_doc( + { + "doctype": "Attendance Request", + "employee": self.employee.name, + "from_date": shift_start, + "to_date": shift_end, + "reason": "On Duty", + "company": "_Test Company", + } + ).save() + + self.assertEqual(attendance_request.shift, shift_type.name) + @HRMSTestSuite.change_settings("HR Settings", {"allow_multiple_shift_assignments": True}) def test_overlap_with_different_shifts(self): shift_1 = create_shift("Morning Shift", "08:00:00", "12:00:00") diff --git a/hrms/hr/doctype/overtime_slip/overtime_slip.py b/hrms/hr/doctype/overtime_slip/overtime_slip.py index d69d360e6d..8e21279f37 100644 --- a/hrms/hr/doctype/overtime_slip/overtime_slip.py +++ b/hrms/hr/doctype/overtime_slip/overtime_slip.py @@ -342,7 +342,6 @@ def _make_salary_slip(self, salary_structure): return make_salary_slip( salary_structure, employee=self.employee, - ignore_permissions=True, posting_date=self.start_date, ) diff --git a/hrms/hr/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/hrms/hr/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 1295071479..674c6e8b0e 100644 --- a/hrms/hr/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/hrms/hr/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -127,30 +127,32 @@ def filter_stats_by_department(self): self.stats_by_employee = filtered_data def generate_filtered_time_logs(self): - additional_filters = "" - - filter_fields = ["employee", "project", "company"] + Timesheet = frappe.qb.DocType("Timesheet") + TimesheetDetail = frappe.qb.DocType("Timesheet Detail") + + query = ( + frappe.qb.from_(TimesheetDetail) + .join(Timesheet) + .on(TimesheetDetail.parent == Timesheet.name) + .select( + Timesheet.employee.as_("employee"), + TimesheetDetail.hours.as_("hours"), + TimesheetDetail.is_billable.as_("is_billable"), + TimesheetDetail.project.as_("project"), + ) + .where(Timesheet.employee.isnotnull()) + .where(Timesheet.start_date[self.from_date : self.to_date]) + .where(Timesheet.end_date[self.from_date : self.to_date]) + ) - for field in filter_fields: + for field in ("employee", "company"): if self.filters.get(field): - if field == "project": - additional_filters += f" AND ttd.{field} = {self.filters.get(field)!r}" - else: - additional_filters += f" AND tt.{field} = {self.filters.get(field)!r}" - - # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql - self.filtered_time_logs = frappe.db.sql( - f""" - SELECT tt.employee AS employee, ttd.hours AS hours, ttd.is_billable AS is_billable, ttd.project AS project - FROM `tabTimesheet Detail` AS ttd - JOIN `tabTimesheet` AS tt - ON ttd.parent = tt.name - WHERE tt.employee IS NOT NULL - AND tt.start_date BETWEEN '{self.filters.from_date}' AND '{self.filters.to_date}' - AND tt.end_date BETWEEN '{self.filters.from_date}' AND '{self.filters.to_date}' - {additional_filters} - """ - ) + query = query.where(Timesheet[field] == self.filters.get(field)) + + if self.filters.get("project"): + query = query.where(TimesheetDetail.project == self.filters.get("project")) + + self.filtered_time_logs = query.run() def generate_stats_by_employee(self): self.stats_by_employee = frappe._dict() diff --git a/hrms/locale/cs.po b/hrms/locale/cs.po index 664bf53696..6442ecee1f 100644 --- a/hrms/locale/cs.po +++ b/hrms/locale/cs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: contact@frappe.io\n" "POT-Creation-Date: 2026-05-24 10:09+0000\n" -"PO-Revision-Date: 2026-05-28 12:58\n" +"PO-Revision-Date: 2026-05-31 13:29\n" "Last-Translator: contact@frappe.io\n" "Language-Team: Czech\n" "MIME-Version: 1.0\n" @@ -11632,7 +11632,7 @@ msgstr "" #: hrms/hr/doctype/employee_advance/employee_advance.py:75 msgid "here" -msgstr "" +msgstr "zde" #: frontend/src/views/Login.vue:16 msgid "johndoe@mail.com" diff --git a/hrms/overrides/employee_master.py b/hrms/overrides/employee_master.py index 8c0e7bb1bd..60c6533c43 100644 --- a/hrms/overrides/employee_master.py +++ b/hrms/overrides/employee_master.py @@ -124,6 +124,9 @@ def get_timeline_data(doctype: str, name: str) -> dict: out = {} + frappe.has_permission(doctype, "read", name, throw=True) + frappe.has_permission("Attendance", "read", throw=True) + open_count = get_open_count(doctype, name) out["count"] = open_count["count"] diff --git a/hrms/payroll/doctype/salary_structure/salary_structure.py b/hrms/payroll/doctype/salary_structure/salary_structure.py index cecbd77bf8..e22afa1023 100644 --- a/hrms/payroll/doctype/salary_structure/salary_structure.py +++ b/hrms/payroll/doctype/salary_structure/salary_structure.py @@ -373,7 +373,6 @@ def make_salary_slip( as_print: bool = False, print_format: str | None = None, for_preview: int = 0, - ignore_permissions: bool = False, lwp_days_corrected: float | None = None, ) -> str | Document: def postprocess(source, target): @@ -402,7 +401,6 @@ def postprocess(source, target): target_doc, postprocess, ignore_child_tables=True, - ignore_permissions=ignore_permissions, cached=True, ) diff --git a/hrms/regional/india/utils.py b/hrms/regional/india/utils.py index 543fd96bfa..528fa88be7 100644 --- a/hrms/regional/india/utils.py +++ b/hrms/regional/india/utils.py @@ -108,7 +108,6 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone salary_structure, employee=employee, for_preview=1, - ignore_permissions=True, posting_date=from_date, ) diff --git a/hrms/subscription_utils.py b/hrms/subscription_utils.py index 8d1fbdfca1..8a9d623ad0 100644 --- a/hrms/subscription_utils.py +++ b/hrms/subscription_utils.py @@ -63,7 +63,7 @@ def get_active_employees() -> int: return frappe.db.count("Employee", {"status": "Active"}) -@frappe.whitelist(allow_guest=True) +@frappe.whitelist() def subscription_updated(app: str, plan: str): if app in ["hrms", "erpnext"] and plan: update_erpnext_access()