Files
ihrm/attendance/models.py

1024 lines
34 KiB
Python

"""
models.py
This module is used to register models for recruitment app
"""
from collections.abc import Iterable
import json
import contextlib
import datetime as dt
from datetime import datetime, date, timedelta
from django.db import models
from django.db.models import Q
from django.core.exceptions import ValidationError
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.db.models.signals import post_save
import pandas as pd
from base.models import Company, EmployeeShift, EmployeeShiftDay, WorkType
from base.horilla_company_manager import HorillaCompanyManager
from employee.models import Employee
from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog
from leave.models import (
WEEK_DAYS,
WEEKS,
CompanyLeave,
Holiday,
LeaveRequest,
LeaveType,
)
from attendance.methods.differentiate import get_diff_dict
# Create your models here.
def strtime_seconds(time):
"""
this method is used to reconvert time in H:M formate string back to seconds and return it
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(":"))))
def format_time(seconds):
"""
This method is used to formate seconds to H:M and return it
args:
seconds : seconds
"""
hour = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = int((seconds % 3600) % 60)
return f"{hour:02d}:{minutes:02d}"
def validate_time_format(value):
"""
this method is used to validate the format of duration like fields.
"""
if len(value) > 6:
raise ValidationError(_("Invalid format, it should be HH:MM format"))
try:
hour, minute = value.split(":")
if len(hour) > 3 or len(minute) > 2:
raise ValidationError(_("Invalid time"))
hour = int(hour)
minute = int(minute)
if len(str(hour)) > 3 or len(str(minute)) > 2 or minute not in range(60):
raise ValidationError(_("Invalid time, excepted MM:SS"))
except ValueError as error:
raise ValidationError(_("Invalid format")) from error
def validate_time_in_minutes(value):
"""
this method is used to validate the format of duration like fields.
"""
if len(value) > 5:
raise ValidationError(_("Invalid format, it should be MM:SS format"))
try:
minutes, sec = value.split(":")
if len(minutes) > 2 or len(sec) > 2:
raise ValidationError(_("Invalid time, excepted MM:SS"))
minutes = int(minutes)
sec = int(sec)
if minutes not in range(60) or sec not in range(60):
raise ValidationError(_("Invalid time, excepted MM:SS"))
except ValueError as e:
raise ValidationError(_("Invalid format, excepted MM:SS")) from e
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.
"""
today = datetime.today().date()
if date > today:
raise ValidationError(_("You cannot choose a future date."))
month_mapping = {
"january": 1,
"february": 2,
"march": 3,
"april": 4,
"may": 5,
"june": 6,
"july": 7,
"august": 8,
"september": 9,
"october": 10,
"november": 11,
"december": 12,
}
class AttendanceActivity(models.Model):
"""
AttendanceActivity model
"""
employee_id = models.ForeignKey(
Employee,
on_delete=models.PROTECT,
related_name="employee_attendance_activities",
verbose_name=_("Employee"),
)
attendance_date = models.DateField(
null=True,
validators=[attendance_date_validate],
verbose_name=_("Attendance Date"),
)
shift_day = models.ForeignKey(
EmployeeShiftDay,
null=True,
on_delete=models.DO_NOTHING,
verbose_name=_("Shift Day"),
)
in_datetime = models.DateTimeField(null=True)
clock_in_date = models.DateField(null=True, verbose_name=_("In Date"))
clock_in = models.TimeField(verbose_name=_("Check In"))
clock_out_date = models.DateField(null=True, verbose_name=_("Out Date"))
out_datetime = models.DateTimeField(null=True)
clock_out = models.TimeField(null=True, verbose_name=_("Check Out"))
objects = HorillaCompanyManager(
related_company_field="employee_id__employee_work_info__company_id"
)
class Meta:
"""
Meta class to add some additional options
"""
ordering = ["-attendance_date", "employee_id__employee_first_name", "clock_in"]
class Attendance(models.Model):
"""
Attendance model
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.first_save = True
status = [
("create_request", _("Create Request")),
("update_request", _("Update Request")),
("revalidate_request", _("Re-validate Request")),
]
employee_id = models.ForeignKey(
Employee,
on_delete=models.PROTECT,
null=True,
related_name="employee_attendances",
verbose_name=_("Employee"),
)
attendance_date = models.DateField(
null=False,
validators=[attendance_date_validate],
verbose_name=_("Attendance date"),
)
shift_id = models.ForeignKey(
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,
verbose_name=_("Work Type"),
)
attendance_day = models.ForeignKey(
EmployeeShiftDay,
on_delete=models.DO_NOTHING,
null=True,
verbose_name=_("Attendance day"),
)
attendance_clock_in_date = models.DateField(
null=True, verbose_name=_("Check-In Date")
)
attendance_clock_in = models.TimeField(
null=True, verbose_name=_("Check-In"), help_text=_("First Check-In Time")
)
attendance_clock_out_date = models.DateField(
null=True, verbose_name=_("Check-Out Date")
)
attendance_clock_out = models.TimeField(
null=True, verbose_name=_("Check-Out"), help_text=_("Last Check-Out Time")
)
attendance_worked_hour = models.CharField(
null=True,
default="00:00",
max_length=10,
validators=[validate_time_format],
verbose_name=_("Worked Hours"),
)
minimum_hour = models.CharField(
max_length=10,
default="00:00",
validators=[validate_time_format],
verbose_name=_("Minimum hour"),
)
attendance_overtime = models.CharField(
default="00:00",
validators=[validate_time_format],
max_length=10,
verbose_name=_("Overtime"),
)
attendance_overtime_approve = models.BooleanField(
default=False, verbose_name=_("Overtime approved")
)
attendance_validated = models.BooleanField(
default=False, verbose_name=_("Attendance validated")
)
at_work_second = models.IntegerField(null=True, blank=True)
overtime_second = models.IntegerField(
null=True, blank=True, verbose_name=_("Overtime In Second")
)
approved_overtime_second = models.IntegerField(default=0)
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,max_length=255)
request_type = models.CharField(
max_length=18, null=True, choices=status, default="update_request"
)
requested_data = models.JSONField(null=True, editable=False)
objects = HorillaCompanyManager(
related_company_field="employee_id__employee_work_info__company_id"
)
created_at = models.DateTimeField(auto_now_add=True, null=True)
history = HorillaAuditLog(
related_name="history_set",
bases=[
HorillaAuditInfo,
],
)
class Meta:
"""
Meta class to add some additional options
"""
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",
]
def __str__(self) -> str:
return f"{self.employee_id.employee_first_name} \
{self.employee_id.employee_last_name} - {self.attendance_date}"
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
def get_last_clock_out(self, null_activity=False):
"""
This method is used to get the last attendance activity if exists
"""
activities = AttendanceActivity.objects.filter(
employee_id=self.employee_id,
attendance_date=self.attendance_date,
clock_out__isnull=null_activity,
).order_by("id")
return activities.last()
def get_at_work_from_activities(self):
"""
This method is used to retun the at work calculated from the activities
"""
activities = AttendanceActivity.objects.filter(
attendance_date=self.attendance_date, employee_id=self.employee_id
).order_by("clock_in")
at_work_seconds = 0
now = datetime.now()
for activity in activities:
out_time = activity.clock_out
if out_time is None:
combined_out = datetime.combine(
now, dt.time(hour=now.hour, minute=now.minute, second=now.second)
)
else:
combined_out = datetime.combine(activity.clock_out_date, out_time)
in_time = activity.clock_in
combined_in = datetime.combine(activity.clock_in_date, in_time)
diffs = combined_out - combined_in
at_work_seconds = at_work_seconds + diffs.total_seconds()
return at_work_seconds
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
def save(self, *args, **kwargs):
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()
)
prev_attendance_approved = False
# Taking all holidays into a list
leaves = []
holidays = Holiday.objects.all()
for holi in holidays:
start_date = holi.start_date
end_date = holi.end_date
# Convert start_date and end_date to datetime objects
start_date = datetime.strptime(str(start_date), "%Y-%m-%d")
end_date = datetime.strptime(str(end_date), "%Y-%m-%d")
# Add dates in between start date and end date including both
current_date = start_date
while current_date <= end_date:
leaves.append(current_date.strftime("%Y-%m-%d"))
current_date += timedelta(days=1)
# Checking attendance date is in holiday list, if found making the minimum hour to 00:00
for leave in leaves:
if str(leave) == str(self.attendance_date):
self.minimum_hour = "00:00"
break
# Making a dictonary contains week day value and leave day pairs
company_leaves = {}
company_leave = CompanyLeave.objects.all()
for com_leave in company_leave:
a = dict(WEEK_DAYS).get(com_leave.based_on_week_day)
b = com_leave.based_on_week
company_leaves[b] = a
# Checking the attendance date is in which week
week_in_month = str(((self.attendance_date.day - 1) // 7 + 1) - 1)
# Checking the attendance date is in the company leave or not
for pairs in company_leaves.items():
# For all weeks based_on_week is None
if str(pairs[0]) == "None":
if str(pairs[1]) == str(self.attendance_day):
self.minimum_hour = "00:00"
break
# Checking with based_on_week and attendance_date week
if str(pairs[0]) == week_in_month:
if str(pairs[1]) == str(self.attendance_day):
self.minimum_hour = "00:00"
break
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)
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)
self.first_save = False
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())
else:
self.create_ot()
self.update_ot(employee_ot.first())
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()
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 if self.shift_id else "",
"work_type_id": self.work_type_id.id if self.work_type_id else "",
"attendance_worked_hour": self.attendance_worked_hour,
"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
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
"""
employee_ot = AttendanceOverTime()
employee_ot.employee_id = self.employee_id
employee_ot.month = self.attendance_date.strftime("%B").lower()
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
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)
hour_balance = 0
hours_pending = 0
minimum_hour_second = 0
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)
employee_ot.save()
return employee_ot
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"
}
)
if (
self.attendance_clock_out_date
and 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"
}
)
if self.attendance_clock_out_date and self.attendance_clock_out_date >= today:
if out_time > now:
raise ValidationError(
{"attendance_clock_out": "Check out time not allow in the future"}
)
class AttendancerequestFile(models.Model):
file = models.FileField(upload_to="attendance/request_files")
class AttendancerequestComment(models.Model):
"""
AttendancerequestComment Model
"""
request_id = models.ForeignKey(Attendance, on_delete=models.CASCADE)
employee_id = models.ForeignKey(Employee, on_delete=models.CASCADE)
files = models.ManyToManyField(AttendancerequestFile, blank=True)
comment = models.TextField(null=True, verbose_name=_("Comment"),max_length=255)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Created At"),
null=True,
)
def __str__(self) -> str:
return f"{self.comment}"
class AttendanceOverTime(models.Model):
"""
AttendanceOverTime model
"""
employee_id = models.ForeignKey(
Employee,
on_delete=models.PROTECT,
related_name="employee_overtime",
verbose_name=_("Employee"),
)
month = models.CharField(
max_length=10,
verbose_name=_("Month"),
)
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 = HorillaCompanyManager(
related_company_field="employee_id__employee_work_info__company_id"
)
class Meta:
"""
Meta class to add some additional options
"""
unique_together = [("employee_id"), ("month"), ("year")]
ordering = ["-year", "-month_sequence"]
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
def not_validated_hrs(self):
"""
This method will return not validated hours in a month
"""
hrs_to_vlaidate = sum(
list(
Attendance.objects.filter(
attendance_date__month=month_mapping[self.month],
attendance_date__year=self.year,
employee_id=self.employee_id,
attendance_validated=False,
).values_list("at_work_second", flat=True)
)
)
return format_time(hrs_to_vlaidate)
def not_approved_ot_hrs(self):
"""
This method will return the overtime hours to be approved
"""
hrs_to_approve = sum(
list(
Attendance.objects.filter(
attendance_date__month=month_mapping[self.month],
attendance_date__year=self.year,
employee_id=self.employee_id,
attendance_validated=True,
attendance_overtime_approve=False,
).values_list("overtime_second", flat=True)
)
)
return format_time(hrs_to_approve)
def get_month_index(self):
"""
This method will return the index of the month
"""
return month_mapping[self.month]
def save(self, *args, **kwargs):
self.hour_account_second = strtime_seconds(self.worked_hours)
self.hour_pending_second = strtime_seconds(self.pending_hours)
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",
]
self.month_sequence = months.index(month_name)
super().save(*args, **kwargs)
class AttendanceLateComeEarlyOut(models.Model):
"""
AttendanceLateComeEarlyOut model
"""
choices = [
("late_come", _("Late Come")),
("early_out", _("Early Out")),
]
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",
verbose_name=_("Employee"),
editable=False,
)
type = models.CharField(max_length=20, choices=choices, verbose_name=_("Type"))
objects = HorillaCompanyManager(
related_company_field="employee_id__employee_work_info__company_id"
)
created_at = models.DateTimeField(auto_now_add=True, null=True)
def get_penalties_count(self):
"""
This method is used to return the total penalties in the late early instance
"""
return self.penaltyaccount_set.count()
def save(self, *args, **kwargs) -> None:
super().save(*args, **kwargs)
self.employee_id = self.attendance_id.employee_id
super().save(*args, **kwargs)
class Meta:
"""
Meta class to add some additional options
"""
unique_together = [("attendance_id"), ("type")]
ordering = ["-attendance_id__attendance_date"]
def __str__(self) -> str:
return f"{self.attendance_id.employee_id.employee_first_name} \
{self.attendance_id.employee_id.employee_last_name} - {self.type}"
class AttendanceValidationCondition(models.Model):
"""
AttendanceValidationCondition model
"""
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]
)
company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company"))
objects = HorillaCompanyManager()
def clean(self):
"""
This method is used to perform some custom validations
"""
super().clean()
if not self.id and AttendanceValidationCondition.objects.exists():
raise ValidationError(_("You cannot add more conditions."))
months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
class PenaltyAccount(models.Model):
"""
LateComeEarlyOutPenaltyAccount
"""
employee_id = models.ForeignKey(
Employee,
on_delete=models.PROTECT,
related_name="penalty_set",
editable=False,
verbose_name="Employee",
null=True,
)
late_early_id = models.ForeignKey(
AttendanceLateComeEarlyOut, on_delete=models.CASCADE, null=True, editable=False
)
leave_request_id = models.ForeignKey(
LeaveRequest, null=True, on_delete=models.CASCADE, editable=False
)
leave_type_id = models.ForeignKey(
LeaveType,
on_delete=models.DO_NOTHING,
blank=True,
null=True,
verbose_name="Leave type",
)
minus_leaves = models.FloatField(default=0.0, null=True)
deduct_from_carry_forward = models.BooleanField(default=False)
penalty_amount = models.FloatField(default=0.0, null=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
def clean(self) -> None:
super().clean()
if not self.leave_type_id and self.minus_leaves:
raise ValidationError(
{"leave_type_id": _("Specify the leave type to deduct the leave.")}
)
if self.leave_type_id and not self.minus_leaves:
raise ValidationError(
{
"minus_leaves": _(
"If a leave type is chosen for a penalty, minus leaves are required."
)
}
)
if not self.minus_leaves and not self.penalty_amount:
raise ValidationError(
{
"leave_type_id": _(
"Either minus leaves or a penalty amount is required"
)
}
)
if (
self.minus_leaves or self.deduct_from_carry_forward
) and not self.leave_type_id:
raise ValidationError({"leave_type_id": _("Leave type is required")})
return
class Meta:
ordering = ["-created_at"]
@receiver(post_save, sender=PenaltyAccount)
def create_initial_stage(sender, instance, created, **kwargs):
"""
This is post save method, used to create initial stage for the recruitment
"""
# only work when creating
if created:
penalty_amount = instance.penalty_amount
if penalty_amount:
from payroll.models.models import Deduction
penalty = Deduction()
if instance.late_early_id:
penalty.title = f"{instance.late_early_id.get_type_display()} penalty"
penalty.one_time_date = (
instance.late_early_id.attendance_id.attendance_date
)
elif instance.leave_request_id:
penalty.title = f"Leave penalty {instance.leave_request_id.end_date}"
penalty.one_time_date = instance.leave_request_id.end_date
else:
penalty.title = f"Penalty on {datetime.today()}"
penalty.one_time_date = datetime.today()
penalty.include_active_employees = False
penalty.is_fixed = True
penalty.amount = instance.penalty_amount
penalty.only_show_under_employee = True
penalty.save()
penalty.include_active_employees = False
penalty.specific_employees.add(instance.employee_id)
penalty.save()
if instance.leave_type_id and instance.minus_leaves:
available = instance.employee_id.available_leave.filter(
leave_type_id=instance.leave_type_id
).first()
unit = round(instance.minus_leaves * 2) / 2
if not instance.deduct_from_carry_forward:
available.available_days = max(0, (available.available_days - unit))
else:
available.carryforward_days = max(
0, (available.carryforward_days - unit)
)
available.save()
class GraceTime(models.Model):
"""
Model for saving Grace time
"""
allowed_time = models.CharField(
default="00:00",
validators=[validate_time_in_minutes],
max_length=10,
verbose_name=_("Allowed time"),
)
allowed_time_in_secs = models.IntegerField()
is_default = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company"))
objects = HorillaCompanyManager()
def __str__(self) -> str:
return str(f"{self.allowed_time} - Minutes")
def clean(self):
"""
This method is used to perform some custom validations
"""
super().clean()
if self.is_default:
if GraceTime.objects.filter(is_default=True).exclude(id=self.id).exists():
raise ValidationError(
_("There is already a default grace time that exists.")
)
allowed_time = self.allowed_time
is_default = self.is_default
exclude_default = not is_default
if (
GraceTime.objects.filter(allowed_time=allowed_time)
.exclude(is_default=exclude_default)
.exclude(id=self.id)
.exists()
):
raise ValidationError(
_("There is already a grace time with this allowed time that exists.")
)
def save(self, *args, **kwargs):
allowed_time = self.allowed_time
minute, secs = allowed_time.split(":")
minute_int = int(minute)
secs_int = int(secs)
minute_str = f"{minute_int:02d}"
secs_str = f"{secs_int:02d}"
self.allowed_time = f"{minute_str}:{secs_str}"
self.allowed_time_in_secs = minute_int * 60 + secs_int
super().save(*args, **kwargs)
class AttendanceGeneralSetting(models.Model):
"""
AttendanceGeneralSettings
"""
time_runner = models.BooleanField(default=True)
company_id = models.ForeignKey(Company,on_delete=models.CASCADE,null=True)