Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f1a572d
fix: add permission check for half-day attendance updates
Jun 20, 2026
a108e07
fix: guard raw query builder update inside permission block
Jun 20, 2026
b62203e
fix: scope qb.update to the exact attendance_name validated by permis…
Jun 20, 2026
63f7b81
fix: filter attendance lookup to submitted records (docstatus=1)
Jun 20, 2026
14fa9b1
fix: use frappe.db.set_value to preserve audit trail on half-day update
Jun 21, 2026
ff7bd4a
test(attendance): add regression test for half-day permission check
Jun 27, 2026
46802ee
test(attendance): fix regression test CI issues
Jun 27, 2026
2bfc553
test(attendance): fix regression test CI issues
Jun 27, 2026
b30f504
fix(attendance): prevalidate permissions before batch update
Jun 27, 2026
3ae9a9b
test: ensure attendance is reloaded before permission check
Jun 28, 2026
89e9d59
fix: resolve test isolation/regression causing CI failures
Jun 28, 2026
6fa808a
chore: remove stray files
Jun 28, 2026
ae5eaec
fix: implement close warning for job openings and related validation.
krishna-254 Jun 25, 2026
212367b
fix(pwa): preserve link field value when value is outside default opt…
iamkhanraheel Jun 30, 2026
41a968b
fix(pwa): show employee number and reports_to in Profile
iamkhanraheel Jul 2, 2026
10e5f16
style: linting issues
deepeshgarg007 Jul 3, 2026
b02860f
fix: Bosnian translations
frappe-pr-bot Jul 3, 2026
4a94ad2
fix: resolve attendance half-day permission and N+1 query issues
Jul 4, 2026
438d85f
fix: handle null late_entry/early_exit in half-day update query
Jul 4, 2026
57bf4b0
fix: add docstatus guard to half-day update WHERE clause
Jul 4, 2026
cda6102
chore: update POT file
frappe-pr-bot Jul 5, 2026
520709a
fix: French translations
frappe-pr-bot Jul 6, 2026
37e83ac
fix: Spanish translations
frappe-pr-bot Jul 6, 2026
dee81eb
fix: Arabic translations
frappe-pr-bot Jul 6, 2026
d6c574e
fix: Bulgarian translations
frappe-pr-bot Jul 6, 2026
c81fc56
fix: Czech translations
frappe-pr-bot Jul 6, 2026
a6600e8
fix: Danish translations
frappe-pr-bot Jul 6, 2026
d59ef57
fix: Korean translations
frappe-pr-bot Jul 6, 2026
c41c806
fix: Persian translations
frappe-pr-bot Jul 6, 2026
1dbdc87
fix: Portuguese, Brazilian translations
frappe-pr-bot Jul 6, 2026
a2db8ea
fix: German translations
frappe-pr-bot Jul 6, 2026
1da037a
fix: Hungarian translations
frappe-pr-bot Jul 6, 2026
a64ba83
fix: Italian translations
frappe-pr-bot Jul 6, 2026
9d18ad0
fix: Dutch translations
frappe-pr-bot Jul 6, 2026
8d0b7aa
fix: Portuguese translations
frappe-pr-bot Jul 6, 2026
5ea9a23
fix: Russian translations
frappe-pr-bot Jul 6, 2026
3a105b2
fix: Slovenian translations
frappe-pr-bot Jul 6, 2026
95e8993
fix: Serbian (Cyrillic) translations
frappe-pr-bot Jul 6, 2026
ec14f02
fix: Turkish translations
frappe-pr-bot Jul 6, 2026
3e51927
fix: Chinese Simplified translations
frappe-pr-bot Jul 6, 2026
9903b3b
fix: Indonesian translations
frappe-pr-bot Jul 6, 2026
bcffc55
fix: Thai translations
frappe-pr-bot Jul 6, 2026
d700b0e
fix: Hindi translations
frappe-pr-bot Jul 6, 2026
6672f7e
fix: Burmese translations
frappe-pr-bot Jul 6, 2026
8659408
fix: Norwegian Bokmal translations
frappe-pr-bot Jul 6, 2026
cde5109
fix: Serbian (Latin) translations
frappe-pr-bot Jul 6, 2026
0d62a08
Merge pull request #4798 from krishna-254/fix/job-opening-error-on-st…
krishna-254 Jul 9, 2026
23aa91b
Merge pull request #4848 from frappe/l10n_develop
deepeshgarg007 Jul 9, 2026
7815501
Merge pull request #4855 from frappe/pot_develop_2026-07-05
deepeshgarg007 Jul 9, 2026
d2b33b3
Merge pull request #4739 from pratheep-bit/fix/attendance-update-perm…
deepeshgarg007 Jul 9, 2026
00a02ee
Merge pull request #4833 from iamkhanraheel/fix/pwa_profile_blank_fields
deepeshgarg007 Jul 9, 2026
e4b2e66
Merge pull request #4808 from iamkhanraheel/fix/link_field_outside_de…
deepeshgarg007 Jul 9, 2026
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
2 changes: 1 addition & 1 deletion frontend/src/components/EmployeeAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
:size="props.size"
/>
<div class="text-base text-gray-800 grow">
{{ employee?.employee_name }}
{{ employee?.employee_name || props.employeeID }}
</div>
</div>

Expand Down
22 changes: 21 additions & 1 deletion frontend/src/components/Link.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const options = createResource({
},
method: "POST",
transform: (data) => {
return data.map((doc) => {
const mapped = data.map((doc) => {
let title = null
if (doc.label && doc.label !== doc.value){
title = doc.label
Expand All @@ -72,6 +72,11 @@ const options = createResource({
value: doc.value,
}
})

if (props.modelValue && !mapped.find((o) => o.value === props.modelValue)) {
mapped.unshift({ label: props.modelValue, value: props.modelValue })
}
return mapped
},
})

Expand Down Expand Up @@ -106,4 +111,19 @@ watch(
() => props.filters,
() => reloadOptions(''),
)

watch(
() => props.modelValue,
(newVal, oldVal) => {
if (!newVal && oldVal) {
// value cleared — reload so the dropdown shows the full default list
searchText.value = ""
reloadOptions("")
} else if (newVal && newVal !== oldVal) {
// reload so transform can inject it if it's outside the default page
const inOptions = (options.data || []).find((o) => o.value === newVal)
if (options.data && !inOptions) reloadOptions("")
}
}
)
</script>
27 changes: 25 additions & 2 deletions frontend/src/views/Profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
const [label, fieldtype] = getFieldInfo(field)
return {
fieldname: field,
value: employeeDoc.doc[field],
value: getFieldValue(field),
label: label,
fieldtype: fieldtype,
}
Expand All @@ -138,7 +138,7 @@
</template>

<script setup>
import { computed, inject, ref, onMounted, onBeforeUnmount } from "vue"
import { computed, inject, ref, watch, onMounted, onBeforeUnmount } from "vue"
import { useRouter } from "vue-router"
import { IonPage, IonContent } from "@ionic/vue"
import { FeatherIcon, createDocumentResource, createResource, toast } from "frappe-ui"
Expand Down Expand Up @@ -244,6 +244,19 @@ const employeeDoc = createDocumentResource({
},
})

const reportsToName = createResource({
url: "hrms.api.get_reports_to_employee_name",
})

watch(
() => employeeDoc.doc?.reports_to,
(reports_to) => {
if (reports_to) {
reportsToName.submit({ employee: reports_to })
}
}
)

const employeeDocType = createResource({
url: "hrms.api.get_doctype_fields",
params: { doctype: DOCTYPE },
Expand All @@ -257,6 +270,16 @@ const getFieldInfo = (fieldname) => {
return [__(field?.label, null, "Employee"), field?.fieldtype]
}

const getFieldValue = (fieldname) => {
if (fieldname === "employee_number" && !employeeDoc.doc[fieldname]) {
return employeeDoc.doc["name"]
}
if (fieldname === "reports_to") {
return reportsToName.data || employeeDoc.doc[fieldname]
}
return employeeDoc.doc[fieldname]
}

const logout = async () => {
try {
await session.logout.submit()
Expand Down
11 changes: 11 additions & 0 deletions hrms/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ def get_all_employees() -> list[dict]:
)


@frappe.whitelist()
def get_reports_to_employee_name(employee: str) -> str:
reports_to = frappe.db.get_value(
"Employee", {"user_id": frappe.session.user, "status": "Active"}, "reports_to"
)
if not reports_to or reports_to != employee:
frappe.throw(_("Not permitted"), frappe.PermissionError)

return frappe.db.get_value("Employee", employee, "employee_name") or ""


def get_current_employee() -> str:
employee = get_current_employee_info().get("name")
if not employee:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,28 @@ def mark_employee_attendance(
frappe.has_permission("Attendance", "write", throw=True)
if isinstance(half_day_employee_list, str):
half_day_employee_list = json.loads(half_day_employee_list)

eligible_attendance = frappe.get_list(
"Attendance",
filters={
"employee": ["in", half_day_employee_list],
"attendance_date": date,
"docstatus": 1,
},
fields=["name", "employee"],
limit=0,
)
attendance_map = {d.employee: d.name for d in eligible_attendance}

Attendance = frappe.qb.DocType("Attendance")
for employee in half_day_employee_list:
frappe.qb.update(Attendance).where(
(Attendance.employee == employee) & (Attendance.attendance_date == date)
).set(Attendance.half_day_status, half_day_status).set(Attendance.shift, shift).set(
Attendance.late_entry, late_entry
).set(Attendance.early_exit, early_exit).set(Attendance.modify_half_day_status, 0).run()
attendance_name = attendance_map.get(employee)
if attendance_name:
frappe.has_permission("Attendance", "write", attendance_name, throw=True)
frappe.qb.update(Attendance).where(
(Attendance.employee == employee)
& (Attendance.attendance_date == date)
& (Attendance.docstatus == 1)
).set(Attendance.half_day_status, half_day_status).set(Attendance.shift, shift).set(
Attendance.late_entry, late_entry or 0
).set(Attendance.early_exit, early_exit or 0).set(Attendance.modify_half_day_status, 0).run()
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,65 @@ def test_get_unmarked_attendance_with_shift(self):
self.assertIn(self.employee2.name, filtered)
self.assertNotIn(self.employee3.name, filtered)

def test_mark_half_day_attendance_permissions(self):
user_no_role = "test_no_role@example.com"

frappe.set_user("Administrator")
make_employee(user_no_role, company="_Test Company")

date = add_days(getdate(), -1)
while is_holiday(employee=self.employee1, date=date):
date = add_days(date, -1)

# Create and submit an attendance record as Administrator
attendance = frappe.get_doc(
{
"doctype": "Attendance",
"employee": self.employee1,
"attendance_date": date,
"status": "Present",
}
).insert()
attendance.submit()

# Scenario 1 (Security): user without Attendance write permission
# Verify the unauthorized user cannot update half-day attendance
frappe.set_user(user_no_role)
try:
# Precondition: verify the user lacks doctype-level write access
self.assertFalse(frappe.has_permission("Attendance", "write"))

with self.assertRaises(frappe.PermissionError):
mark_employee_attendance(
employee_list=[],
status="Present",
date=date,
mark_half_day=True,
half_day_status="Absent",
half_day_employee_list=[self.employee1],
)
finally:
frappe.set_user("Administrator")

# Verify the record was NOT modified
attendance.reload()
self.assertIsNone(attendance.half_day_status)
self.assertEqual(attendance.status, "Present")

# Scenario 2 (Positive): Administrator can update half-day attendance
mark_employee_attendance(
employee_list=[],
status="Present",
date=date,
mark_half_day=True,
half_day_status="Absent",
half_day_employee_list=[self.employee1],
)

attendance.reload()
self.assertEqual(attendance.half_day_status, "Absent")
self.assertEqual(attendance.status, "Present")


def create_leave_allocation(employee, leave_type, date=None):
from_date = add_days(date or getdate(), -2)
Expand Down
29 changes: 29 additions & 0 deletions hrms/hr/doctype/job_opening/job_opening.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ frappe.ui.form.on("Job Opening", {
company: function (frm) {
frm.set_value("designation", "");
},
status: function (frm) {
frm.close_warning_confirmed = false;
},
before_save: function (frm) {
frm.trigger("confirm_close_if_needed");
},
after_save: function (frm) {
frm.close_warning_confirmed = false;
},
confirm_close_if_needed: function (frm) {
if (frm.is_new() || frm.close_warning_confirmed || frm.doc.status !== "Closed") {
return;
}

frappe.validated = false;

frm.call("get_close_warning").then((r) => {
if (!r.message) {
frm.close_warning_confirmed = true;
frm.save();
return;
}

frappe.confirm(r.message, () => {
frm.close_warning_confirmed = true;
frm.save();
});
});
},

job_opening_template: function (frm) {
if (!frm.doc.job_opening_template) return;
Expand Down
25 changes: 25 additions & 0 deletions hrms/hr/doctype/job_opening/job_opening.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,26 @@ def validate_dates(self):
if self.status == "Closed":
self.validate_from_to_dates("posted_on", "closed_on")

@frappe.whitelist()
def get_close_warning(self):
if not self.is_opening_being_closed():
return

if not self.staffing_plan:
return

return _(
"Closing this Job Opening for {1} may not be according to the Staffing Plan {0}.<br><br>"
"Do you want to close this Job Opening?"
).format(
frappe.bold(get_link_to_form("Staffing Plan", self.staffing_plan)),
frappe.bold(self.designation),
)

def validate_current_vacancies(self):
if self.status == "Closed":
return

if not self.staffing_plan:
staffing_plan = get_active_staffing_plan_details(self.company, self.designation)
if staffing_plan:
Expand Down Expand Up @@ -123,6 +142,12 @@ def validate_current_vacancies(self):
title=_("Vacancies fulfilled"),
)

def is_opening_being_closed(self):
if self.is_new() or self.status != "Closed":
return False

return frappe.db.get_value(self.doctype, self.name, "status") == "Open"

def update_job_requisition_status(self):
if self.status == "Closed" and self.job_requisition:
job_requisition = frappe.get_doc("Job Requisition", self.job_requisition)
Expand Down
64 changes: 64 additions & 0 deletions hrms/hr/doctype/job_opening/test_job_opening.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,70 @@ def test_vacancies_fulfilled(self):
opening_1.status = "Closed"
opening_1.save()

def test_close_job_opening_after_employee_creation(self):
staffing_plan = frappe.get_doc(
{
"doctype": "Staffing Plan",
"company": "_Test Opening Company",
"name": "Test",
"from_date": getdate(),
"to_date": add_days(getdate(), 10),
}
)

staffing_plan.append(
"staffing_details",
{"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000},
)
staffing_plan.insert()
staffing_plan.submit()

opening = get_job_opening()
opening.insert()

make_employee(
"test_job_opening_after_hiring@example.com",
company="_Test Opening Company",
designation="Designer",
)

opening.status = "Closed"
close_warning = opening.get_close_warning()
self.assertIn("may not be according to the Staffing Plan", close_warning)
self.assertNotIn("<table", close_warning)
self.assertIn("Designer", close_warning)
self.assertIn("<strong><a", close_warning)
opening.save()

self.assertEqual(opening.status, "Closed")

def test_close_job_opening_under_staffing_plan_shows_warning(self):
staffing_plan = frappe.get_doc(
{
"doctype": "Staffing Plan",
"company": "_Test Opening Company",
"name": "Test",
"from_date": getdate(),
"to_date": add_days(getdate(), 10),
}
)

staffing_plan.append(
"staffing_details",
{"designation": "Designer", "vacancies": 3, "estimated_cost_per_position": 50000},
)
staffing_plan.insert()
staffing_plan.submit()

opening = get_job_opening(job_title="Designer Under Plan")
opening.insert()

opening.status = "Closed"
self.assertIn("may not be according to the Staffing Plan", opening.get_close_warning())
opening.save()

self.assertEqual(opening.status, "Closed")

def test_close_expired_job_openings(self):
today = getdate()

Expand Down
Loading
Loading