Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions hrms/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -424,13 +429,15 @@ 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,
["leave_approver", "department"],
)

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},
Expand Down Expand Up @@ -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"]

Expand Down Expand Up @@ -600,13 +608,15 @@ 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,
["expense_approver", "department"],
)

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},
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions hrms/api/roster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 3 additions & 1 deletion hrms/hr/doctype/attendance_request/attendance_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
38 changes: 38 additions & 0 deletions hrms/hr/doctype/attendance_request/test_attendance_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 0 additions & 1 deletion hrms/hr/doctype/overtime_slip/overtime_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions hrms/locale/cs.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions hrms/overrides/employee_master.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
2 changes: 0 additions & 2 deletions hrms/payroll/doctype/salary_structure/salary_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -402,7 +401,6 @@ def postprocess(source, target):
target_doc,
postprocess,
ignore_child_tables=True,
ignore_permissions=ignore_permissions,
cached=True,
)

Expand Down
1 change: 0 additions & 1 deletion hrms/regional/india/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
2 changes: 1 addition & 1 deletion hrms/subscription_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading