Skip to content

Commit 3260af6

Browse files
authored
Merge branch 'develop' into patch-1
2 parents e7d11cd + 2174174 commit 3260af6

42 files changed

Lines changed: 4953 additions & 4154 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/stale.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@ jobs:
1616
days-before-pr-close: 3
1717
stale-pr-label: 'Inactive'
1818
exempt-draft-pr: true
19-
days-before-issue-stale: -1
20-
days-before-issue-close: -1
19+
days-before-issue-stale: 5
20+
days-before-issue-close: 2
21+
stale-issue-label: 'Inactive'
22+
any-of-issue-labels: "question,can't replicate"
23+
remove-issue-stale-when-updated: true
24+
labels-to-remove-when-unstale: 'Inactive'
2125
stale-pr-message: |
2226
This pull request is being marked as inactive because of no recent activity.
2327
If your PR hasn't been reviewed, it's likely because it doesn't fullfill the [contribution guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines). Please read them carefully and fix the pull request. When you are sure all items are checked, please ping relevant codeowner in the comment. Be nice, they have a lot on their plate too.
2428
2529
It will be closed in 3 days if no further activity occurs.
2630
Thank you for contributing!
31+
stale-issue-message: |
32+
Hi, this is your friendly neighbourhood bot :)
33+
34+
Thank you for taking time to report the issue, however your description of the issue is insufficient to understand the exact problem and/or to replicate and fix it. Please provide the information requested by the maintainers, so this can be fixed. More the steps/screenshots/videos the better.
35+
36+
It will be closed in 2 days if no further activity occurs.
37+
Beep Boop!

hrms/hr/doctype/attendance/attendance.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"fieldname": "working_hours",
8282
"fieldtype": "Float",
8383
"label": "Working Hours",
84-
"precision": "1",
84+
"precision": "2",
8585
"read_only": 1
8686
},
8787
{
@@ -259,7 +259,7 @@
259259
"idx": 1,
260260
"is_submittable": 1,
261261
"links": [],
262-
"modified": "2025-07-10 11:39:04.327505",
262+
"modified": "2025-12-16 17:44:13.859387",
263263
"modified_by": "Administrator",
264264
"module": "HR",
265265
"name": "Attendance",

hrms/hr/doctype/overtime_type/test_overtime_type.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def create_overtime_type(**args):
2121
args = frappe._dict(args)
2222

2323
overtime_type = frappe.new_doc("Overtime Type")
24-
overtime_type.name = "_Test Overtime"
24+
overtime_type.name = args.get("name") or "_Test Overtime"
2525
overtime_type.overtime_calculation_method = args.overtime_calculation_method or "Salary Component Based"
2626
overtime_type.standard_multiplier = 1
2727
overtime_type.applicable_for_weekend = args.applicable_for_weekend or 0

hrms/hr/doctype/shift_assignment/shift_assignment.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,8 @@ def get_shift_details(shift_type_name: str, for_timestamp: datetime | None = Non
601601

602602
actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time)
603603
actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time)
604+
allow_overtime = shift_type.allow_overtime
605+
overtime_type = shift_type.overtime_type
604606

605607
return frappe._dict(
606608
{
@@ -609,6 +611,8 @@ def get_shift_details(shift_type_name: str, for_timestamp: datetime | None = Non
609611
"end_datetime": end_datetime,
610612
"actual_start": actual_start,
611613
"actual_end": actual_end,
614+
"allow_overtime": allow_overtime,
615+
"overtime_type": overtime_type,
612616
}
613617
)
614618

@@ -623,6 +627,8 @@ def get_shift_type(shift_type_name: str) -> dict:
623627
"end_time",
624628
"begin_check_in_before_shift_start_time",
625629
"allow_check_out_after_shift_end_time",
630+
"allow_overtime",
631+
"overtime_type",
626632
],
627633
as_dict=1,
628634
)

hrms/hr/doctype/shift_assignment/test_shift_assignment.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
22
# See license.txt
33

4+
from datetime import timedelta
5+
46
import frappe
5-
from frappe.utils import add_days, get_datetime, getdate, nowdate
7+
from frappe.utils import add_days, get_datetime, getdate, now_datetime, nowdate
68

79
from erpnext.setup.doctype.employee.test_employee import make_employee
810

11+
from hrms.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
12+
from hrms.hr.doctype.overtime_type.test_overtime_type import create_overtime_type
913
from hrms.hr.doctype.shift_assignment.shift_assignment import (
1014
MultipleShiftError,
1115
OverlappingShiftError,
1216
get_actual_start_end_datetime_of_shift,
1317
get_events,
1418
)
1519
from hrms.hr.doctype.shift_type.test_shift_type import make_shift_assignment, setup_shift_type
20+
from hrms.payroll.doctype.salary_component.test_salary_component import create_salary_component
1621
from hrms.tests.utils import HRMSTestSuite
1722

1823
test_dependencies = ["Shift Type"]
@@ -251,3 +256,52 @@ def test_shift_details_on_consecutive_days_with_overlapping_timings(self):
251256
self.assertTrue(checkin.shift_type.name == checkout.shift_type.name == "Morning")
252257
self.assertEqual(checkin.actual_start, get_datetime(f"{yesterday} 06:00:00"))
253258
self.assertEqual(checkout.actual_end, get_datetime(f"{yesterday} 13:00:00"))
259+
260+
def test_auto_attendance_calculates_ot_for_default_shift(self):
261+
"""Ensure overtime is calculated when employee works beyond default shift hours."""
262+
salary_component = create_salary_component("Overtime")
263+
264+
overtime_type = create_overtime_type(
265+
name="_Test Overtime Type",
266+
maximum_overtime_hours_allowed=5,
267+
overtime_calculation_method="Fixed Hourly Rate",
268+
overtime_salary_component=salary_component.name,
269+
)
270+
271+
shift_type = setup_shift_type(
272+
shift_type="_Test OT Shift",
273+
start_time="08:00:00",
274+
end_time="17:00:00",
275+
allow_overtime=1,
276+
overtime_type=overtime_type.name,
277+
enable_auto_attendance=1,
278+
allow_check_out_after_shift_end_time=300,
279+
last_sync_of_checkin=now_datetime() + timedelta(days=2),
280+
)
281+
282+
employee = make_employee(
283+
"test_ot_default_shift@example.com",
284+
company="_Test Company",
285+
default_shift=shift_type.name,
286+
)
287+
288+
make_checkin(employee, get_datetime(f"{getdate()} 08:00:00"))
289+
make_checkin(employee, get_datetime(f"{getdate()} 19:00:00"), log_type="OUT")
290+
291+
shift_type.process_auto_attendance()
292+
293+
attendance = frappe.db.get_value(
294+
"Attendance",
295+
{
296+
"employee": employee,
297+
"attendance_date": getdate(),
298+
"docstatus": ["!=", 2],
299+
},
300+
["overtime_type", "working_hours", "actual_overtime_duration"],
301+
as_dict=True,
302+
)
303+
304+
self.assertIsNotNone(attendance)
305+
self.assertEqual(attendance.overtime_type, shift_type.overtime_type)
306+
self.assertEqual(attendance.working_hours, 11.0)
307+
self.assertEqual(attendance.actual_overtime_duration, 2.0)

hrms/hr/doctype/shift_assignment_tool/test_shift_assignment_tool.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def test_get_employees_for_assigning_shift_schedule(self):
112112

113113
def test_get_shift_requests(self):
114114
today = getdate()
115+
setup_shift_type(shift_type="Day Shift")
115116

116117
for emp in [self.emp1, self.emp2, self.emp3, self.emp4, self.emp5]:
117118
employee = frappe.get_doc("Employee", emp)
@@ -251,6 +252,7 @@ def test_bulk_process_shift_requests(self):
251252
employee.shift_request_approver = "employee1@test.com"
252253
employee.save()
253254

255+
setup_shift_type(shift_type="Day Shift")
254256
request1 = make_shift_request(
255257
employee=self.emp1,
256258
employee_name="employee1@test.com",

hrms/hr/doctype/shift_type/shift_type.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,14 @@
8181
"fieldname": "working_hours_threshold_for_half_day",
8282
"fieldtype": "Float",
8383
"label": "Working Hours Threshold for Half Day",
84-
"precision": "1"
84+
"precision": "2"
8585
},
8686
{
8787
"description": "Working hours below which Absent is marked. (Zero to disable)",
8888
"fieldname": "working_hours_threshold_for_absent",
8989
"fieldtype": "Float",
9090
"label": "Working Hours Threshold for Absent",
91-
"precision": "1"
91+
"precision": "2"
9292
},
9393
{
9494
"default": "60",
@@ -210,7 +210,7 @@
210210
}
211211
],
212212
"links": [],
213-
"modified": "2024-12-18 19:03:38.278336",
213+
"modified": "2025-12-16 16:32:49.169920",
214214
"modified_by": "Administrator",
215215
"module": "HR",
216216
"name": "Shift Type",
@@ -251,8 +251,9 @@
251251
}
252252
],
253253
"quick_entry": 1,
254+
"row_format": "Dynamic",
254255
"sort_field": "creation",
255256
"sort_order": "DESC",
256257
"states": [],
257258
"track_changes": 1
258-
}
259+
}

hrms/hr/doctype/shift_type/test_shift_type.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,52 @@ def test_bg_job_creation_for_large_checkins(self):
868868
job = frappe.get_all("RQ Job", {"job_id": f"process_auto_attendance_{shift.name}"})
869869
self.assertTrue(job)
870870

871+
def test_precision_for_working_hours_threshold(self):
872+
shift = setup_shift_type(
873+
start_time="10:00:00",
874+
end_time="18:00:00",
875+
working_hours_threshold_for_half_day=4.75,
876+
working_hours_threshold_for_absent=1.25,
877+
)
878+
employee = make_employee(
879+
"test_working_hours@example.com", company="_Test Company", default_shift=shift.name
880+
)
881+
from hrms.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
882+
883+
in_time = datetime.combine(getdate(), get_time("10:00:00"))
884+
make_checkin(employee, in_time)
885+
886+
# checked out before completing absent hours threshold
887+
out_time = datetime.combine(getdate(), get_time("11:14:00"))
888+
check_out = make_checkin(employee, out_time)
889+
shift.process_auto_attendance()
890+
attendance = frappe.get_doc(
891+
"Attendance", {"employee": employee, "shift": shift.name, "attendance_date": getdate()}
892+
)
893+
self.assertEqual(attendance.status, "Absent")
894+
attendance.cancel()
895+
896+
# barely passed absent hour threshold
897+
check_out.time = datetime.combine(getdate(), get_time("11:15:00"))
898+
check_out.save()
899+
shift.process_auto_attendance()
900+
attendance = frappe.get_doc(
901+
"Attendance", {"employee": employee, "shift": shift.name, "attendance_date": getdate()}
902+
)
903+
self.assertEqual(attendance.status, "Half Day")
904+
self.assertEqual(attendance.working_hours, 1.25)
905+
attendance.cancel()
906+
907+
# barely passed half day hour threshold
908+
check_out.time = datetime.combine(getdate(), get_time("14:45:00"))
909+
check_out.save()
910+
shift.process_auto_attendance()
911+
attendance = frappe.get_doc(
912+
"Attendance", {"employee": employee, "shift": shift.name, "attendance_date": getdate()}
913+
)
914+
self.assertEqual(attendance.status, "Present")
915+
self.assertEqual(attendance.working_hours, 4.75)
916+
871917

872918
def setup_shift_type(**args):
873919
args = frappe._dict(args)

hrms/hr/doctype/staffing_plan/staffing_plan.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ def set_total_estimated_budget(self):
3939

4040
for detail in self.get("staffing_details"):
4141
# Set readonly fields
42-
self.set_number_of_positions(detail)
4342
designation_counts = get_designation_counts(detail.designation, self.company)
4443
detail.current_count = designation_counts["employee_count"]
4544
detail.current_openings = designation_counts["job_openings"]
46-
45+
self.set_number_of_positions(detail)
4746
detail.total_estimated_cost = 0
4847
if detail.number_of_positions > 0:
4948
if detail.vacancies and detail.estimated_cost_per_position:
@@ -181,12 +180,14 @@ def set_job_requisitions(self, job_reqs):
181180

182181
self.staffing_details = []
183182
for req in requisitions:
183+
current_count = get_designation_counts(req.designation, self.company)["employee_count"]
184184
self.append(
185185
"staffing_details",
186186
{
187187
"designation": req.designation,
188188
"vacancies": req.no_of_positions,
189189
"estimated_cost_per_position": req.expected_compensation,
190+
"number_of_positions": cint(current_count) + cint(req.no_of_positions),
190191
},
191192
)
192193

hrms/hr/doctype/staffing_plan/test_staffing_plan.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33

44
import frappe
55
from frappe.tests import IntegrationTestCase
6-
from frappe.utils import add_days, nowdate
6+
from frappe.utils import add_days, get_first_day, get_last_day, getdate, nowdate
7+
8+
from erpnext.setup.doctype.employee.test_employee import make_employee
79

810
from hrms.hr.doctype.staffing_plan.staffing_plan import ParentCompanyError, SubsidiaryCompanyError
911

1012
test_dependencies = ["Designation"]
1113

1214

1315
class TestStaffingPlan(IntegrationTestCase):
16+
def setUp(self):
17+
for doctype in ["Staffing Plan", "Staffing Plan Detail"]:
18+
frappe.db.delete(doctype)
19+
make_company()
20+
1421
def test_staffing_plan(self):
15-
_set_up()
1622
frappe.db.set_value("Company", "_Test Company 3", "is_group", 1)
1723
if frappe.db.exists("Staffing Plan", "Test"):
1824
return
@@ -45,7 +51,6 @@ def test_staffing_plan_subsidiary_company(self):
4551
self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert)
4652

4753
def test_staffing_plan_parent_company(self):
48-
_set_up()
4954
if frappe.db.exists("Staffing Plan", "Test"):
5055
return
5156
staffing_plan = frappe.new_doc("Staffing Plan")
@@ -74,11 +79,32 @@ def test_staffing_plan_parent_company(self):
7479
staffing_plan.insert()
7580
self.assertRaises(ParentCompanyError, staffing_plan.submit)
7681

82+
def test_staffing_details_from_job_requisition(self):
83+
from hrms.hr.doctype.job_requisition.test_job_requisition import make_job_requisition
7784

78-
def _set_up():
79-
for doctype in ["Staffing Plan", "Staffing Plan Detail"]:
80-
frappe.db.sql(f"delete from `tab{doctype}`")
81-
make_company()
85+
employee = make_employee("test_sp@example.com", company="_Test Company", designation="Accountant")
86+
requisition = make_job_requisition(requested_by=employee, designation="Accountant", no_of_positions=4)
87+
staffing_plan = frappe.get_doc(
88+
{
89+
"doctype": "Staffing Plan",
90+
"__newname": "Test JR",
91+
"company": "_Test Company",
92+
"from_date": get_first_day(getdate()),
93+
"to_date": get_last_day(getdate()),
94+
}
95+
)
96+
staffing_plan.set_job_requisitions([requisition.name])
97+
staffing_plan.save()
98+
staffing_plan_detail = frappe.db.get_values(
99+
"Staffing Plan Detail",
100+
{"parent": staffing_plan.name},
101+
["designation", "vacancies", "current_count", "number_of_positions"],
102+
as_dict=True,
103+
)[0]
104+
self.assertEqual(staffing_plan_detail.designation, "Accountant")
105+
self.assertEqual(staffing_plan_detail.vacancies, 4)
106+
self.assertEqual(staffing_plan_detail.current_count, 1)
107+
self.assertEqual(staffing_plan_detail.number_of_positions, 5)
82108

83109

84110
def make_company(name=None, abbr=None):

0 commit comments

Comments
 (0)