Files
ihrm/attendance/models.py

429 lines
14 KiB
Python
Raw Normal View History

"""
models.py
This module is used to register models for recruitment app
"""
import contextlib
from datetime import datetime
2023-05-10 15:06:57 +05:30
from django.db import models
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
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,
on_delete=models.CASCADE,
related_name="employee_attendance_activities",
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)
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
2023-05-10 15:06:57 +05:30
employee_id = models.ForeignKey(
Employee,
on_delete=models.CASCADE,
null=True,
related_name="employee_attendances",
)
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_date = models.DateField(
2023-08-02 14:27:23 +05:30
null=False,
validators=[attendance_date_validate],
verbose_name=_("Attendance date"),
)
attendance_day = models.ForeignKey(
EmployeeShiftDay, on_delete=models.DO_NOTHING, null=True
)
2023-07-11 12:12:25 +05:30
attendance_clock_in = models.TimeField(
2023-08-02 14:27:23 +05:30
null=True, verbose_name=_("Check-in"), help_text="First Check-in Time"
)
attendance_clock_in_date = models.DateField(
null=True, verbose_name=_("Check-in date")
2023-07-11 12:12:25 +05:30
)
attendance_clock_out = models.TimeField(
2023-08-02 14:27:23 +05:30
null=True, verbose_name=_("Check-out"), help_text="Last Check-out Time"
2023-07-11 12:12:25 +05:30
)
attendance_clock_out_date = models.DateField(
2023-08-02 14:27:23 +05:30
null=True, verbose_name=_("Check-out date")
)
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],
2023-08-02 14:27:23 +05:30
verbose_name=_("At work"),
)
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-05-10 15:06:57 +05:30
attendance_validated = models.BooleanField(default=False)
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-07-11 12:12:25 +05:30
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 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 condition 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
2023-07-11 12:12:25 +05:30
self_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()
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 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()
# 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
"""
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,
)
2023-05-10 15:06:57 +05:30
hour_balance = 0
for attendance in month_attendances:
hour_balance = hour_balance + attendance.at_work_second
employee_ot.hour_account = format_time(hour_balance)
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(
{
2023-07-11 12:12:25 +05:30
"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(
{
2023-07-11 12:12:25 +05:30
"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,
on_delete=models.CASCADE,
related_name="employee_overtime",
verbose_name="Employee",
)
2023-05-10 15:06:57 +05:30
month = models.CharField(max_length=10)
month_sequence = models.PositiveSmallIntegerField(default=0)
year = models.CharField(
default=datetime.now().strftime("%Y"), null=True, max_length=10
)
hour_account = models.CharField(
max_length=10, default="00:00", null=True, validators=[validate_time_format]
)
overtime = models.CharField(
max_length=20, default="00:00", validators=[validate_time_format]
)
hour_account_second = models.IntegerField(
default=0,
null=True,
)
overtime_second = models.IntegerField(
default=0,
null=True,
)
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 save(self, *args, **kwargs):
self.hour_account_second = strtime_seconds(self.hour_account)
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.CASCADE, related_name="late_come_early_out"
)
employee_id = models.ForeignKey(
Employee,
on_delete=models.DO_NOTHING,
null=True,
related_name="late_come_early_out",
verbose_name="Employee",
)
type = models.CharField(max_length=20, choices=choices)
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(
default="09:00", max_length=10, validators=[validate_time_format]
)
minimum_overtime_to_approve = models.CharField(
default="00:30", null=True, max_length=10, validators=[validate_time_format]
)
overtime_cutoff = models.CharField(
default="02:00", null=True, max_length=10, validators=[validate_time_format]
)
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."))