Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: validation for circular shift times (backport #2783) #2799

Merged
merged 4 commits into from
Feb 20, 2025
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
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
Loading