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

feat: Scheduling Multiple shifts and Auto Attendance #29955

Merged
merged 34 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3711119
refactor: overlapping shifts validation
ruchamahabal Feb 17, 2022
625a9f6
refactor: consider timeslots in `get_employee_shift`
ruchamahabal Feb 20, 2022
ace3f8a
fix: handle shift grace overlap while finding current shift
ruchamahabal Feb 20, 2022
62e7275
refactor: handle shifts spanning over 2 different days
ruchamahabal Feb 20, 2022
e7cbb5f
fix: fetching shift on timing boundaries
ruchamahabal Feb 21, 2022
f6a12a9
refactor: rewrite docstrings and add type hints for functions
ruchamahabal Feb 21, 2022
cb3b330
refactor: Allow multiple attendance records creation for different sh…
ruchamahabal Feb 21, 2022
742c8f0
feat: auto attendance marking for multiple shifts on the same day
ruchamahabal Feb 22, 2022
4ef2911
refactor: mark absent for employees with no attendance
ruchamahabal Feb 23, 2022
f2ee365
chore: sort imports, remove unused imports
ruchamahabal Feb 23, 2022
e79d292
refactor: Monthly Attendance Sheet
ruchamahabal Mar 27, 2022
865204a
feat: add colors for attendance status to lessen the cognitive load
ruchamahabal Mar 27, 2022
41cfcdb
feat: show shift-wise attendance in monthly attendance sheet
ruchamahabal Mar 29, 2022
acb2743
test: monthly attendance sheet
ruchamahabal Mar 30, 2022
baec607
style: format code with black
ruchamahabal Mar 30, 2022
0a3cf64
chore: ignore formatting changes in blame
ruchamahabal Mar 30, 2022
72501f2
Merge branch 'develop' into multiple-shifts
ruchamahabal Mar 30, 2022
af13919
test: fetching shifts in Employee Checkins
ruchamahabal Mar 30, 2022
97547da
fix(test): make holiday list for shift and checkin tests
ruchamahabal Mar 30, 2022
e4cc0c1
fix: tests
ruchamahabal Mar 30, 2022
d45e286
test: shift assignment creation
ruchamahabal Mar 31, 2022
655c1dd
fix: attendance fixes
ruchamahabal Apr 3, 2022
6fffdcf
test: Shift Type with Auto Attendance setup and working
ruchamahabal Apr 3, 2022
58fb2f7
refactor: Overlapping validation for Shift Request
ruchamahabal Apr 3, 2022
7bd84f2
chore: remove unused import
ruchamahabal Apr 3, 2022
83489be
fix: add validation for overlapping shift attendance
ruchamahabal Apr 4, 2022
277bda1
test: validations for duplicate and overlapping shift attendance records
ruchamahabal Apr 4, 2022
62cdde9
test: skip auto attendance
ruchamahabal Apr 4, 2022
1d4b1c4
fix: skip validation for overlapping shift attendance if no shift is …
ruchamahabal Apr 4, 2022
d9f0040
Merge branch 'develop' into multiple-shifts
ruchamahabal Apr 4, 2022
7b406f4
Merge branch 'develop' into multiple-shifts
ruchamahabal Apr 4, 2022
fec4763
test: add holiday related shift and attendance tests
ruchamahabal Apr 4, 2022
bd077db
test: add attendance sheet tests for employee filter, half days
ruchamahabal Apr 4, 2022
0e15283
fix: sider
ruchamahabal Apr 4, 2022
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
3 changes: 3 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a

# bulk format python code with black
494bd9ef78313436f0424b918f200dab8fc7c20b

# bulk format python code with black
baec607ff5905b1c67531096a9cf50ec7ff00a5d
169 changes: 134 additions & 35 deletions erpnext/hr/doctype/attendance/attendance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
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


class DuplicateAttendanceError(frappe.ValidationError):
pass


class OverlappingShiftAttendanceError(frappe.ValidationError):
pass


class Attendance(Document):
def validate(self):
from erpnext.controllers.status_updater import validate_status
Expand All @@ -18,6 +28,7 @@ def validate(self):
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()

Expand All @@ -35,21 +46,35 @@ def validate_attendance_date(self):
frappe.throw(_("Attendance date can not be less than employee's joining date"))

def validate_duplicate_record(self):
res = frappe.db.sql(
"""
select name from `tabAttendance`
where employee = %s
and attendance_date = %s
and name != %s
and docstatus != 2
""",
(self.employee, getdate(self.attendance_date), self.name),
duplicate = get_duplicate_attendance_record(
self.employee, self.attendance_date, self.shift, self.name
)
if res:

if duplicate:
frappe.throw(
_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)
)
_("Attendance for employee {0} is already marked for the date {1}: {2}").format(
frappe.bold(self.employee),
frappe.bold(self.attendance_date),
get_link_to_form("Attendance", duplicate[0].name),
),
title=_("Duplicate Attendance"),
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):
Expand Down Expand Up @@ -103,6 +128,69 @@ def validate_employee(self):
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))


def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
attendance = frappe.qb.DocType("Attendance")
query = (
frappe.qb.from_(attendance)
.select(attendance.name)
.where((attendance.employee == employee) & (attendance.docstatus < 2))
)

if shift:
query = query.where(
Criterion.any(
[
Criterion.all(
[
((attendance.shift.isnull()) | (attendance.shift == "")),
(attendance.attendance_date == attendance_date),
]
),
Criterion.all(
[
((attendance.shift.isnotnull()) | (attendance.shift != "")),
(attendance.attendance_date == attendance_date),
(attendance.shift == shift),
]
),
]
)
)
else:
query = query.where((attendance.attendance_date == attendance_date))

if name:
query = query.where(attendance.name != name)

return query.run(as_dict=True)


def get_overlapping_shift_attendance(employee, attendance_date, shift, name=None):
if not shift:
return {}

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 = []
Expand Down Expand Up @@ -141,28 +229,39 @@ def add_attendance(events, start, end, conditions=None):


def mark_attendance(
employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False
employee,
attendance_date,
status,
shift=None,
leave_type=None,
ignore_validate=False,
late_entry=False,
early_exit=False,
):
if not frappe.db.exists(
"Attendance",
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
):
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,
}
)
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()
Expand Down
109 changes: 104 additions & 5 deletions erpnext/hr/doctype/attendance/test_attendance.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate

from erpnext.hr.doctype.attendance.attendance import (
DuplicateAttendanceError,
OverlappingShiftAttendanceError,
get_month_map,
get_unmarked_days,
mark_attendance,
Expand All @@ -23,11 +25,112 @@ def setUp(self):
from_date = get_year_start(getdate())
to_date = get_year_ending(getdate())
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
frappe.db.delete("Attendance")

def test_duplicate_attendance(self):
employee = make_employee("test_duplicate_attendance@example.com", company="_Test Company")
date = nowdate()

mark_attendance(employee, date, "Present")
attendance = frappe.get_doc(
{
"doctype": "Attendance",
"employee": employee,
"attendance_date": date,
"status": "Absent",
"company": "_Test Company",
}
)

self.assertRaises(DuplicateAttendanceError, attendance.insert)

def test_duplicate_attendance_with_shift(self):
from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type

employee = make_employee("test_duplicate_attendance@example.com", company="_Test Company")
date = nowdate()

shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
mark_attendance(employee, date, "Present", shift=shift_1.name)

# attendance record with shift
attendance = frappe.get_doc(
{
"doctype": "Attendance",
"employee": employee,
"attendance_date": date,
"status": "Absent",
"company": "_Test Company",
"shift": shift_1.name,
}
)

self.assertRaises(DuplicateAttendanceError, attendance.insert)

# attendance record without any shift
attendance = frappe.get_doc(
{
"doctype": "Attendance",
"employee": employee,
"attendance_date": date,
"status": "Absent",
"company": "_Test Company",
}
)

self.assertRaises(DuplicateAttendanceError, attendance.insert)

def test_overlapping_shift_attendance_validation(self):
from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type

employee = make_employee("test_overlap_attendance@example.com", company="_Test Company")
date = nowdate()

shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
shift_2 = setup_shift_type(shift_type="Shift 2", start_time="09:30:00", end_time="11:00:00")

mark_attendance(employee, date, "Present", shift=shift_1.name)

# attendance record with overlapping shift
attendance = frappe.get_doc(
{
"doctype": "Attendance",
"employee": employee,
"attendance_date": date,
"status": "Absent",
"company": "_Test Company",
"shift": shift_2.name,
}
)

self.assertRaises(OverlappingShiftAttendanceError, attendance.insert)

def test_allow_attendance_with_different_shifts(self):
# allows attendance with 2 different non-overlapping shifts
from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type

employee = make_employee("test_duplicate_attendance@example.com", company="_Test Company")
date = nowdate()

shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
shift_2 = setup_shift_type(shift_type="Shift 2", start_time="11:00:00", end_time="12:00:00")

mark_attendance(employee, date, "Present", shift_1.name)
frappe.get_doc(
{
"doctype": "Attendance",
"employee": employee,
"attendance_date": date,
"status": "Absent",
"company": "_Test Company",
"shift": shift_2.name,
}
).insert()

def test_mark_absent(self):
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
frappe.db.delete("Attendance", {"employee": employee, "attendance_date": date})

attendance = mark_attendance(employee, date, "Absent")
fetch_attendance = frappe.get_value(
"Attendance", {"employee": employee, "attendance_date": date, "status": "Absent"}
Expand All @@ -42,7 +145,6 @@ def test_unmarked_days(self):
employee = make_employee(
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
)
frappe.db.delete("Attendance", {"employee": employee})
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)

first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
Expand All @@ -67,8 +169,6 @@ def test_unmarked_days_excluding_holidays(self):
employee = make_employee(
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
)
frappe.db.delete("Attendance", {"employee": employee})

frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)

first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
Expand All @@ -95,7 +195,6 @@ def test_unmarked_days_as_per_joining_and_relieving_dates(self):
employee = make_employee(
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
)
frappe.db.delete("Attendance", {"employee": employee})

frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)

Expand Down
Loading