diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index a2487b31ff..bcaeae4fe5 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -8,6 +8,7 @@ from frappe.model.document import Document 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 @@ class DuplicateAttendanceError(frappe.ValidationError): pass +class OverlappingShiftAttendanceError(frappe.ValidationError): + pass + + class Attendance(Document): def validate(self): from erpnext.controllers.status_updater import validate_status @@ -23,6 +28,7 @@ class Attendance(Document): 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 @@ class Attendance(Document): 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 @@ def get_duplicate_attendance_record(employee, attendance_date, shift, name=None) 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 @@ def mark_attendance( 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 662b236222..64eb019b00 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 import _ 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 @@ def mark_attendance_and_link_log( 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,