fix: add validation for overlapping shift attendance
- skip auto attendance in case of overlapping shift attendance record
- this case won't occur in case of shift assignment, since it will not allow overlapping shifts to be assigned
- can happen if manual attendance records are created
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index a2487b3..bcaeae4 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -8,6 +8,7 @@
from frappe.query_builder import Criterion
from frappe.utils import cint, cstr, formatdate, get_datetime, get_link_to_form, getdate, nowdate
+from erpnext.hr.doctype.shift_assignment.shift_assignment import has_overlapping_timings
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
@@ -15,6 +16,10 @@
pass
+class OverlappingShiftAttendanceError(frappe.ValidationError):
+ pass
+
+
class Attendance(Document):
def validate(self):
from erpnext.controllers.status_updater import validate_status
@@ -23,6 +28,7 @@
validate_active_employee(self.employee)
self.validate_attendance_date()
self.validate_duplicate_record()
+ self.validate_overlapping_shift_attendance()
self.validate_employee_status()
self.check_leave_record()
@@ -55,6 +61,22 @@
exc=DuplicateAttendanceError,
)
+ def validate_overlapping_shift_attendance(self):
+ attendance = get_overlapping_shift_attendance(
+ self.employee, self.attendance_date, self.shift, self.name
+ )
+
+ if attendance:
+ frappe.throw(
+ _("Attendance for employee {0} is already marked for an overlapping shift {1}: {2}").format(
+ frappe.bold(self.employee),
+ frappe.bold(attendance.shift),
+ get_link_to_form("Attendance", attendance.name),
+ ),
+ title=_("Overlapping Shift Attendance"),
+ exc=OverlappingShiftAttendanceError,
+ )
+
def validate_employee_status(self):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
@@ -143,6 +165,29 @@
return query.run(as_dict=True)
+def get_overlapping_shift_attendance(employee, attendance_date, shift, name=None):
+ attendance = frappe.qb.DocType("Attendance")
+ query = (
+ frappe.qb.from_(attendance)
+ .select(attendance.name, attendance.shift)
+ .where(
+ (attendance.employee == employee)
+ & (attendance.docstatus < 2)
+ & (attendance.attendance_date == attendance_date)
+ & (attendance.shift != shift)
+ )
+ )
+
+ if name:
+ query = query.where(attendance.name != name)
+
+ overlapping_attendance = query.run(as_dict=True)
+
+ if overlapping_attendance and has_overlapping_timings(shift, overlapping_attendance[0].shift):
+ return overlapping_attendance[0]
+ return {}
+
+
@frappe.whitelist()
def get_events(start, end, filters=None):
events = []
@@ -190,25 +235,30 @@
late_entry=False,
early_exit=False,
):
- if not get_duplicate_attendance_record(employee, attendance_date, shift):
- company = frappe.db.get_value("Employee", employee, "company")
- attendance = frappe.get_doc(
- {
- "doctype": "Attendance",
- "employee": employee,
- "attendance_date": attendance_date,
- "status": status,
- "company": company,
- "shift": shift,
- "leave_type": leave_type,
- "late_entry": late_entry,
- "early_exit": early_exit,
- }
- )
- attendance.flags.ignore_validate = ignore_validate
- attendance.insert()
- attendance.submit()
- return attendance.name
+ if get_duplicate_attendance_record(employee, attendance_date, shift):
+ return
+
+ if get_overlapping_shift_attendance(employee, attendance_date, shift):
+ return
+
+ company = frappe.db.get_value("Employee", employee, "company")
+ attendance = frappe.get_doc(
+ {
+ "doctype": "Attendance",
+ "employee": employee,
+ "attendance_date": attendance_date,
+ "status": status,
+ "company": company,
+ "shift": shift,
+ "leave_type": leave_type,
+ "late_entry": late_entry,
+ "early_exit": early_exit,
+ }
+ )
+ attendance.flags.ignore_validate = ignore_validate
+ attendance.insert()
+ attendance.submit()
+ return attendance.name
@frappe.whitelist()
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index 662b236..64eb019 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -7,7 +7,10 @@
from frappe.model.document import Document
from frappe.utils import cint, get_datetime
-from erpnext.hr.doctype.attendance.attendance import get_duplicate_attendance_record
+from erpnext.hr.doctype.attendance.attendance import (
+ get_duplicate_attendance_record,
+ get_overlapping_shift_attendance,
+)
from erpnext.hr.doctype.shift_assignment.shift_assignment import (
get_actual_start_end_datetime_of_shift,
)
@@ -137,7 +140,10 @@
return None
elif attendance_status in ("Present", "Absent", "Half Day"):
employee_doc = frappe.get_doc("Employee", employee)
- if not get_duplicate_attendance_record(employee, attendance_date, shift):
+ duplicate = get_duplicate_attendance_record(employee, attendance_date, shift)
+ overlapping = get_overlapping_shift_attendance(employee, attendance_date, shift)
+
+ if not duplicate and not overlapping:
doc_dict = {
"doctype": "Attendance",
"employee": employee,