Skip to content
Open
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
23 changes: 16 additions & 7 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@
#
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs

# Replace use of Class.extend with native JS class
1fe891b287a1b3f225d29ee3d07e7b1824aba9e7

# This commit just changes spaces to tabs for indentation in some files
5f473611bd6ed57703716244a054d3fb5ba9cd23

# Whitespace trimming throughout codebase
9bb69e711a5da43aaf8c8ecb5601aeffd89dbe5a
f0bcb753fb7ebbb64bb0d6906d431d002f0f7d8f
# Whitespace fix throughout codebase
4551d7d6029b6f587f6c99d4f8df5519241c6a86
b147b85e6ac19a9220cd1e2958a6ebd99373283a

# sort and cleanup imports
915b34391c2066dfc83e60a5813c5a877cebe7ac

# removing six compatibility layer
8fe5feb6a4372bf5f2dfaf65fca41bbcc25c8ce7

# imports cleanup
4b2be2999f2203493b49bf74c5b440d49e38b5e3
# bulk format python code with black
494bd9ef78313436f0424b918f200dab8fc7c20b

# formatting with black
c07713b860505211db2af685e2e950bf5dd7dd3a
# bulk format python code with black
baec607ff5905b1c67531096a9cf50ec7ff00a5d
193 changes: 130 additions & 63 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,12 +28,10 @@ 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()

def on_cancel(self):
self.unlink_attendance_from_checkins()

def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")

Expand All @@ -38,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 @@ -105,34 +127,68 @@ def validate_employee(self):
if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))

def unlink_attendance_from_checkins(self):
from frappe.utils import get_link_to_form

EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
linked_logs = (
frappe.qb.from_(EmployeeCheckin)
.select(EmployeeCheckin.name)
.where(EmployeeCheckin.attendance == self.name)
.for_update()
.run(as_dict=True)
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 linked_logs:
(
frappe.qb.update(EmployeeCheckin)
.set("attendance", "")
.where(EmployeeCheckin.attendance == self.name)
).run()
if name:
query = query.where(attendance.name != name)

frappe.msgprint(
msg=_("Unlinked Attendance record from Employee Checkins: {}").format(
", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs)
),
title=_("Unlinked logs"),
indicator="blue",
is_minimizable=True,
wide=True,
)
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()
Expand Down Expand Up @@ -173,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 Expand Up @@ -292,4 +359,4 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
if date_time not in marked_days:
unmarked_days.append(date)

return unmarked_days
return unmarked_days
Loading