Skip to content

Commit

Permalink
Merge pull request #2799 from frappe/mergify/bp/version-15-hotfix/pr-…
Browse files Browse the repository at this point in the history
…2783

fix: validation for circular shift times (backport #2783)
  • Loading branch information
asmitahase authored Feb 20, 2025
2 parents 4ed7138 + 9d97503 commit c40d0ce
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 1 deletion.
57 changes: 56 additions & 1 deletion hrms/hr/doctype/shift_type/shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, create_batch, get_datetime, get_time, getdate
from frappe.utils import add_days, cint, create_batch, get_datetime, get_time, getdate, time_diff

from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
Expand All @@ -27,6 +27,61 @@

class ShiftType(Document):
def validate(self):
start = get_time(self.start_time)
end = get_time(self.end_time)
self.validate_same_start_and_end(start, end)
self.validate_circular_shift(start, end)
self.validate_unlinked_logs()

def validate_same_start_and_end(self, start_time: datetime.time, end_time: datetime.time):
if start_time == end_time:
frappe.throw(
title=_("Invalid Shift Times"),
msg=_("Start time and end time cannot be same."),
)

def validate_circular_shift(self, start_time: datetime.time, end_time: datetime.time):
shift_start, shift_end = self.get_shift_start_and_shift_end(start_time, end_time)
if self.get_total_shift_duration_in_minutes(shift_start, shift_end) >= 1440:
max_label = self.get_max_shift_buffer_label()
frappe.throw(
title=_("Invalid Shift Times"),
msg=_("Please reduce {0} to avoid shift time overlapping with itself").format(
frappe.bold(max_label)
),
)

def get_shift_start_and_shift_end(
self, start_time: datetime.time, end_time: datetime.time
) -> tuple[datetime]:
shift_start = datetime.combine(getdate(), start_time)
if start_time < end_time:
shift_end = datetime.combine(getdate(), end_time)
elif start_time > end_time:
shift_end = datetime.combine(add_days(getdate(), 1), end_time)
return shift_start, shift_end

def get_total_shift_duration_in_minutes(
self, shift_start: datetime.time, shift_end: datetime.time
) -> int:
return (
(round(time_diff(shift_end, shift_start).total_seconds() / 60))
+ self.allow_check_out_after_shift_end_time
+ self.begin_check_in_before_shift_start_time
)

def get_max_shift_buffer_label(self) -> str:
labels = {
self.meta.get_label(
"allow_check_out_after_shift_end_time"
): self.allow_check_out_after_shift_end_time,
self.meta.get_label(
"begin_check_in_before_shift_start_time"
): self.begin_check_in_before_shift_start_time,
}
return max(labels, key=labels.get)

def validate_unlinked_logs(self):
if self.is_field_modified("start_time") and self.unlinked_checkins_exist():
frappe.throw(
title=_("Unmarked Check-in Logs Found"),
Expand Down
38 changes: 38 additions & 0 deletions hrms/hr/doctype/shift_type/test_shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,44 @@ def test_validation_for_unlinked_logs_before_changing_important_shift_configurat
get_time(frappe.get_value("Shift Type", shift.name, "start_time")), get_time("10:15:00")
)

def test_circular_shift_times(self):
# single day shift
shift_type = frappe.get_doc(
{
"doctype": "Shift Type",
"__newname": "Test Shift Validation",
"start_time": "09:00:00",
"end_time": "18:00:00",
"enable_auto_attendance": 1,
"determine_check_in_and_check_out": "Alternating entries as IN and OUT during the same shift",
"working_hours_calculation_based_on": "First Check-in and Last Check-out",
"begin_check_in_before_shift_start_time": 500,
"allow_check_out_after_shift_end_time": 500,
"process_attendance_after": add_days(getdate(), -2),
"last_sync_of_checkin": now_datetime() + timedelta(days=1),
}
)

self.assertRaises(frappe.ValidationError, shift_type.save)

# two day shift
shift_type = frappe.get_doc(
{
"doctype": "Shift Type",
"__newname": "Test Shift Validation",
"start_time": "18:00:00",
"end_time": "03:00:00",
"enable_auto_attendance": 1,
"determine_check_in_and_check_out": "Alternating entries as IN and OUT during the same shift",
"working_hours_calculation_based_on": "First Check-in and Last Check-out",
"begin_check_in_before_shift_start_time": 500,
"allow_check_out_after_shift_end_time": 500,
"process_attendance_after": add_days(getdate(), -2),
"last_sync_of_checkin": now_datetime() + timedelta(days=1),
}
)
self.assertRaises(frappe.ValidationError, shift_type.save)


def setup_shift_type(**args):
args = frappe._dict(args)
Expand Down

0 comments on commit c40d0ce

Please sign in to comment.