Files
ihrm/attendance/models.py

589 lines
20 KiB
Python
Raw Normal View History

"""
models.py
This module is used to register models for recruitment app
"""
import json
import contextlib
from datetime import datetime, date, timedelta
2023-05-10 15:06:57 +05:30
from django.db import models
from django.db.models import Q
2023-05-10 15:06:57 +05:30
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from base.models import EmployeeShift, EmployeeShiftDay, WorkType
from employee.models import Employee
from leave.models import LeaveRequest
from attendance.methods.differentiate import get_diff_dict
2023-05-10 15:06:57 +05:30
# Create your models here.
2023-05-10 15:06:57 +05:30
def strtime_seconds(time):
"""
2023-07-11 12:12:25 +05:30
this method is used to reconvert time in H:M formate string back to seconds and return it
2023-05-10 15:06:57 +05:30
args:
time : time in H:M format
"""
ftr = [3600, 60, 1]
return sum(a * b for a, b in zip(ftr, map(int, time.split(":"))))
2023-05-10 15:06:57 +05:30
def format_time(seconds):
2023-07-11 12:12:25 +05:30
"""
This method is used to formate seconds to H:M and return it
2023-05-10 15:06:57 +05:30
args:
seconds : seconds
"""
hour = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = int((seconds % 3600) % 60)
return f"{hour:02d}:{minutes:02d}"
2023-05-10 15:06:57 +05:30
def validate_time_format(value):
"""
2023-05-10 15:06:57 +05:30
this method is used to validate the format of duration like fields.
"""
2023-05-10 15:06:57 +05:30
if len(value) > 6:
raise ValidationError(_("Invalid format, it should be HH:MM format"))
try:
hour, minute = value.split(":")
hour = int(hour)
minute = int(minute)
if len(str(hour)) > 3 or minute not in range(60):
raise ValidationError(_("Invalid time"))
except ValueError as error:
raise ValidationError(_("Invalid format")) from error
2023-05-10 15:06:57 +05:30
def attendance_date_validate(date):
"""
Validates if the provided date is not a future date.
:param date: The date to validate.
:raises ValidationError: If the provided date is in the future.
"""
2023-05-10 15:06:57 +05:30
today = datetime.today().date()
if date > today:
raise ValidationError(_("You cannot choose a future date."))
2023-05-10 15:06:57 +05:30
class AttendanceActivity(models.Model):
"""
AttendanceActivity model
"""
2023-07-05 11:46:47 +05:30
employee_id = models.ForeignKey(
Employee,
2023-09-20 15:02:36 +05:30
on_delete=models.PROTECT,
related_name="employee_attendance_activities",
2023-08-14 14:44:47 +05:30
verbose_name=_("Employee"),
)
2023-05-10 15:06:57 +05:30
attendance_date = models.DateField(null=True, validators=[attendance_date_validate])
clock_in_date = models.DateField(null=True)
shift_day = models.ForeignKey(
EmployeeShiftDay, null=True, on_delete=models.DO_NOTHING
)
2023-05-10 15:06:57 +05:30
clock_in = models.TimeField()
clock_out = models.TimeField(null=True)
clock_out_date = models.DateField(null=True)
objects = models.Manager()
2023-05-10 15:06:57 +05:30
class Meta:
"""
Meta class to add some additional options
"""
2023-07-05 11:46:47 +05:30
ordering = ["-attendance_date", "employee_id__employee_first_name", "clock_in"]
2023-05-10 15:06:57 +05:30
class Attendance(models.Model):
"""
Attendance model
"""
2023-07-05 11:46:47 +05:30
status = [
2023-08-14 14:44:47 +05:30
("create_request", _("Create Request")),
("update_request", _("Update Request")),
("revalidate_request", _("Re-validate Request")),
]
2023-05-10 15:06:57 +05:30
employee_id = models.ForeignKey(
Employee,
2023-09-20 15:02:36 +05:30
on_delete=models.PROTECT,
null=True,
related_name="employee_attendances",
2023-08-14 14:44:47 +05:30
verbose_name=_("Employee"),
)
attendance_date = models.DateField(
null=False,
validators=[attendance_date_validate],
verbose_name=_("Attendance date"),
)
2023-05-10 15:06:57 +05:30
shift_id = models.ForeignKey(
2023-08-02 14:27:23 +05:30
EmployeeShift, on_delete=models.DO_NOTHING, null=True, verbose_name=_("Shift")
)
work_type_id = models.ForeignKey(
WorkType,
null=True,
blank=True,
on_delete=models.DO_NOTHING,
2023-08-02 14:27:23 +05:30
verbose_name=_("Work Type"),
)
attendance_day = models.ForeignKey(
EmployeeShiftDay,
on_delete=models.DO_NOTHING,
null=True,
verbose_name=_("Attendance day"),
)
2023-08-02 14:27:23 +05:30
attendance_clock_in_date = models.DateField(
2023-10-17 10:20:24 +05:30
null=True, verbose_name=_("Check-In Date")
2023-07-11 12:12:25 +05:30
)
attendance_clock_in = models.TimeField(
2023-10-17 10:20:24 +05:30
null=True, verbose_name=_("Check-In"), help_text=_("First Check-In Time")
2023-07-11 12:12:25 +05:30
)
attendance_clock_out_date = models.DateField(
2023-10-17 10:20:24 +05:30
null=True, verbose_name=_("Check-Out Date")
)
attendance_clock_out = models.TimeField(
2023-10-17 10:20:24 +05:30
null=True, verbose_name=_("Check-Out"), help_text=_("Last Check-Out Time")
)
attendance_worked_hour = models.CharField(
2023-07-11 12:12:25 +05:30
null=True,
default="00:00",
max_length=10,
validators=[validate_time_format],
verbose_name=_("Worked Hours"),
)
minimum_hour = models.CharField(
2023-08-02 14:27:23 +05:30
max_length=10,
default="00:00",
validators=[validate_time_format],
verbose_name=_("Minimum hour"),
)
attendance_overtime = models.CharField(
2023-07-11 12:12:25 +05:30
default="00:00",
validators=[validate_time_format],
max_length=10,
2023-08-02 14:27:23 +05:30
verbose_name=_("Overtime"),
2023-07-11 12:12:25 +05:30
)
attendance_overtime_approve = models.BooleanField(
2023-08-02 14:27:23 +05:30
default=False, verbose_name=_("Overtime approved")
)
2023-08-14 14:44:47 +05:30
attendance_validated = models.BooleanField(
default=False, verbose_name=_("Attendance validated")
)
at_work_second = models.IntegerField(null=True, blank=True)
2023-05-10 15:06:57 +05:30
overtime_second = models.IntegerField(null=True, blank=True)
approved_overtime_second = models.IntegerField(default=0)
2023-08-14 14:44:47 +05:30
is_validate_request = models.BooleanField(
default=False, verbose_name=_("Is validate request")
)
is_validate_request_approved = models.BooleanField(
default=False, verbose_name=_("Is validate request approved")
)
request_description = models.TextField(null=True)
2023-08-14 14:44:47 +05:30
request_type = models.CharField(
max_length=18, null=True, choices=status, default="update_request"
)
requested_data = models.JSONField(null=True, editable=False)
objects = models.Manager()
2023-05-10 15:06:57 +05:30
class Meta:
"""
Meta class to add some additional options
"""
2023-07-05 11:46:47 +05:30
unique_together = ("employee_id", "attendance_date")
permissions = [
("change_validateattendance", "Validate Attendance"),
("change_approveovertime", "Change Approve Overtime"),
]
ordering = [
"-attendance_date",
"employee_id__employee_first_name",
"attendance_clock_in",
]
2023-05-10 15:06:57 +05:30
def __str__(self) -> str:
return f"{self.employee_id.employee_first_name} \
{self.employee_id.employee_last_name} - {self.attendance_date}"
2023-05-10 15:06:57 +05:30
def requested_fields(self):
"""
This method will returns the value difference fields
"""
keys = []
if self.requested_data is not None:
data = json.loads(self.requested_data)
diffs = get_diff_dict(self.serialize(), data)
keys = diffs.keys()
return keys
# return f"Attendance ID: {self.id}" # Adjust the representation as needed
def hours_pending(self):
"""
This method will returns difference between minimum_hour and attendance_worked_hour
"""
minimum_hours = strtime_seconds(self.minimum_hour)
worked_hour = strtime_seconds(self.attendance_worked_hour)
pending_seconds = minimum_hours - worked_hour
if pending_seconds < 0:
return "00:00"
else:
pending_hours = format_time(pending_seconds)
return pending_hours
2023-05-10 15:06:57 +05:30
def save(self, *args, **kwargs):
2023-07-11 12:12:25 +05:30
minimum_hour = self.minimum_hour
self_at_work = self.attendance_worked_hour
self.attendance_overtime = format_time(
max(0, (strtime_seconds(self_at_work) - strtime_seconds(minimum_hour)))
)
self_overtime = self.attendance_overtime
self.at_work_second = strtime_seconds(self_at_work)
self.overtime_second = strtime_seconds(self_overtime)
self.attendance_day = EmployeeShiftDay.objects.get(
day=self.attendance_date.strftime("%A").lower()
)
2023-05-10 15:06:57 +05:30
prev_attendance_approved = False
condition = AttendanceValidationCondition.objects.first()
if self.is_validate_request:
self.is_validate_request_approved = False
self.attendance_validated = False
if condition is not None and condition.overtime_cutoff is not None:
overtime_cutoff = condition.overtime_cutoff
cutoff_seconds = strtime_seconds(overtime_cutoff)
overtime = self.overtime_second
if overtime > cutoff_seconds:
self.overtime_second = cutoff_seconds
self.attendance_overtime = format_time(cutoff_seconds)
2023-05-10 15:06:57 +05:30
if self.pk is not None:
# Get the previous values of the boolean field
prev_state = Attendance.objects.get(pk=self.pk)
prev_attendance_approved = prev_state.attendance_overtime_approve
super().save(*args, **kwargs)
employee_ot = self.employee_id.employee_overtime.filter(
month=self.attendance_date.strftime("%B").lower(),
year=self.attendance_date.strftime("%Y"),
)
2023-05-10 15:06:57 +05:30
if employee_ot.exists():
self.update_ot(employee_ot.first())
else:
self.create_ot()
self.update_ot(employee_ot.first())
2023-05-10 15:06:57 +05:30
approved = self.attendance_overtime_approve
attendance_account = self.employee_id.employee_overtime.filter(
month=self.attendance_date.strftime("%B").lower(),
year=self.attendance_date.year,
).first()
2023-05-10 15:06:57 +05:30
total_ot_seconds = attendance_account.overtime_second
if approved and prev_attendance_approved is False:
self.approved_overtime_second = self.overtime_second
total_ot_seconds = total_ot_seconds + self.approved_overtime_second
elif not approved:
total_ot_seconds = total_ot_seconds - self.approved_overtime_second
self.approved_overtime_second = 0
attendance_account.overtime = format_time(total_ot_seconds)
attendance_account.save()
super().save(*args, **kwargs)
def serialize(self):
"""
Used to serialize attendance instance
"""
# Return a dictionary containing the data you want to store
# strftime("%d %b %Y") date
# strftime("%I:%M %p") time
serialized_data = {
"employee_id": self.employee_id.id,
"attendance_date": str(self.attendance_date),
"attendance_clock_in_date": str(self.attendance_clock_in_date),
"attendance_clock_in": str(self.attendance_clock_in),
"attendance_clock_out": str(self.attendance_clock_out),
"attendance_clock_out_date": str(self.attendance_clock_out_date),
"shift_id": self.shift_id.id,
"work_type_id": self.work_type_id.id,
"attendance_worked_hour": self.attendance_worked_hour,
2023-08-14 14:44:47 +05:30
"minimum_hour": self.minimum_hour,
# Add other fields you want to store
}
return serialized_data
def delete(self, *args, **kwargs):
# Custom delete logic
# Perform additional operations before deleting the object
with contextlib.suppress(Exception):
AttendanceActivity.objects.filter(
attendance_date=self.attendance_date, employee_id=self.employee_id
).delete()
employee_ot = self.employee_id.employee_overtime.filter(
month=self.attendance_date.strftime("%B").lower(),
year=self.attendance_date.strftime("%Y"),
)
if employee_ot.exists():
self.update_ot(employee_ot.first())
# Call the superclass delete() method to delete the object
super().delete(*args, **kwargs)
# Perform additional operations after deleting the object
2023-05-10 15:06:57 +05:30
def create_ot(self):
"""
this method is used to create new AttendanceOvertime's instance if there
is no existing for a specific month and year
"""
2023-05-10 15:06:57 +05:30
employee_ot = AttendanceOverTime()
employee_ot.employee_id = self.employee_id
employee_ot.month = self.attendance_date.strftime("%B").lower()
2023-05-10 15:06:57 +05:30
employee_ot.year = self.attendance_date.year
if self.attendance_overtime_approve:
employee_ot.overtime = self.attendance_overtime
if self.attendance_validated:
employee_ot.hour_account = self.attendance_worked_hour
employee_ot.save()
return self
2023-05-10 15:06:57 +05:30
def update_ot(self, employee_ot):
"""
This method is used to update the overtime
Args:
employee_ot (obj): AttendanceOverTime instance
"""
approved_leave_requests = self.employee_id.leaverequest_set.filter(
start_date__lte=self.attendance_date,
end_date__gte=self.attendance_date,
status="approved",
)
# Create a Q object to combine multiple conditions for the exclude clause
exclude_condition = Q()
for leave_request in approved_leave_requests:
exclude_condition |= Q(
attendance_date__range=(
leave_request.start_date,
leave_request.end_date,
)
)
# Filter Attendance objects
month_attendances = Attendance.objects.filter(
employee_id=self.employee_id,
attendance_date__month=self.attendance_date.month,
attendance_date__year=self.attendance_date.year,
attendance_validated=True,
).exclude(exclude_condition)
2023-05-10 15:06:57 +05:30
hour_balance = 0
hours_pending = 0
minimum_hour_second = 0
2023-05-10 15:06:57 +05:30
for attendance in month_attendances:
required_work_second = strtime_seconds(attendance.minimum_hour)
at_work_second = min(
required_work_second,
attendance.at_work_second,
)
hour_balance = hour_balance + at_work_second
minimum_hour_second += strtime_seconds(attendance.minimum_hour)
hours_pending = minimum_hour_second - hour_balance
employee_ot.worked_hours = format_time(hour_balance)
employee_ot.pending_hours = format_time(hours_pending)
2023-05-10 15:06:57 +05:30
employee_ot.save()
return employee_ot
2023-07-05 11:46:47 +05:30
def clean(self, *args, **kwargs):
super().clean(*args, **kwargs)
now = datetime.now().time()
today = datetime.today().date()
out_time = self.attendance_clock_out
if self.attendance_clock_in_date < self.attendance_date:
raise ValidationError(
{
"attendance_clock_in_date": "Attendance check-in date never smaller than attendance date"
2023-07-05 11:46:47 +05:30
}
)
if self.attendance_clock_out_date < self.attendance_clock_in_date:
raise ValidationError(
{
"attendance_clock_out_date": "Attendance check-out date never smaller than attendance check-in date"
2023-07-05 11:46:47 +05:30
}
)
if self.attendance_clock_out_date >= today:
if out_time > now:
raise ValidationError(
{"attendance_clock_out": "Check out time not allow in the future"}
)
2023-05-10 15:06:57 +05:30
class AttendanceOverTime(models.Model):
"""
AttendanceOverTime model
"""
2023-07-05 11:46:47 +05:30
employee_id = models.ForeignKey(
Employee,
2023-09-20 15:02:36 +05:30
on_delete=models.PROTECT,
related_name="employee_overtime",
2023-08-14 14:44:47 +05:30
verbose_name=_("Employee"),
)
month = models.CharField(
max_length=10,
verbose_name=_("Month"),
)
2023-05-10 15:06:57 +05:30
month_sequence = models.PositiveSmallIntegerField(default=0)
year = models.CharField(
default=datetime.now().strftime("%Y"),
null=True,
max_length=10,
verbose_name=_("Year"),
)
worked_hours = models.CharField(
max_length=10,
default="00:00",
null=True,
validators=[validate_time_format],
verbose_name=_("Worked Hours"),
)
pending_hours = models.CharField(
max_length=10,
default="00:00",
null=True,
validators=[validate_time_format],
verbose_name=_("Pending Hours"),
)
overtime = models.CharField(
max_length=20,
default="00:00",
validators=[validate_time_format],
verbose_name=_("Overtime Hours"),
)
hour_account_second = models.IntegerField(
default=0,
null=True,
verbose_name=_("Worked Seconds"),
)
hour_pending_second = models.IntegerField(
default=0,
null=True,
verbose_name=_("Pending Seconds"),
)
overtime_second = models.IntegerField(
default=0,
null=True,
verbose_name=_("Overtime Seconds"),
)
objects = models.Manager()
2023-05-10 15:06:57 +05:30
class Meta:
"""
Meta class to add some additional options
"""
2023-07-05 11:46:47 +05:30
unique_together = [("employee_id"), ("month"), ("year")]
ordering = ["-year", "-month_sequence"]
2023-05-10 15:06:57 +05:30
def month_days(self):
"""
this method is used to create new AttendanceOvertime's instance if there
is no existing for a specific month and year
"""
month = self.month_sequence + 1
year = int(self.year)
start_date = date(year, month, 1)
if month == 12:
end_date = date(year + 1, 1, 1) - timedelta(days=1)
else:
end_date = date(year, month + 1, 1) - timedelta(days=1)
return start_date, end_date
2023-05-10 15:06:57 +05:30
def save(self, *args, **kwargs):
self.hour_account_second = strtime_seconds(self.worked_hours)
self.hour_pending_second = strtime_seconds(self.pending_hours)
2023-05-10 15:06:57 +05:30
self.overtime_second = strtime_seconds(self.overtime)
month_name = self.month.split("-")[0]
months = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
]
2023-05-10 15:06:57 +05:30
self.month_sequence = months.index(month_name)
super().save(*args, **kwargs)
2023-05-10 15:06:57 +05:30
class AttendanceLateComeEarlyOut(models.Model):
"""
AttendanceLateComeEarlyOut model
"""
2023-07-05 11:46:47 +05:30
2023-05-10 15:06:57 +05:30
choices = [
("late_come", _("Late Come")),
("early_out", _("Early Out")),
2023-05-10 15:06:57 +05:30
]
attendance_id = models.ForeignKey(
Attendance,
on_delete=models.PROTECT,
related_name="late_come_early_out",
verbose_name=_("Attendance"),
)
employee_id = models.ForeignKey(
Employee,
on_delete=models.DO_NOTHING,
null=True,
related_name="late_come_early_out",
2023-08-14 14:44:47 +05:30
verbose_name=_("Employee"),
)
type = models.CharField(max_length=20, choices=choices, verbose_name=_("Type"))
objects = models.Manager()
2023-05-10 15:06:57 +05:30
class Meta:
"""
Meta class to add some additional options
"""
2023-07-05 11:46:47 +05:30
unique_together = [("attendance_id"), ("type")]
2023-05-10 15:06:57 +05:30
def __str__(self) -> str:
return f"{self.attendance_id.employee_id.employee_first_name} \
{self.attendance_id.employee_id.employee_last_name} - {self.type}"
2023-05-10 15:06:57 +05:30
class AttendanceValidationCondition(models.Model):
"""
AttendanceValidationCondition model
"""
2023-07-05 11:46:47 +05:30
validation_at_work = models.CharField(
max_length=10, validators=[validate_time_format]
)
minimum_overtime_to_approve = models.CharField(
blank=True, null=True, max_length=10, validators=[validate_time_format]
)
overtime_cutoff = models.CharField(
blank=True, null=True, max_length=10, validators=[validate_time_format]
)
objects = models.Manager()
2023-05-10 15:06:57 +05:30
def clean(self):
"""
This method is used to perform some custom validations
"""
2023-05-10 15:06:57 +05:30
super().clean()
if not self.id and AttendanceValidationCondition.objects.exists():
raise ValidationError(_("You cannot add more conditions."))