diff --git a/attendance/apps.py b/attendance/apps.py index 5608eecc0..392d772d3 100644 --- a/attendance/apps.py +++ b/attendance/apps.py @@ -8,9 +8,14 @@ class AttendanceConfig(AppConfig): def ready(self): from django.urls import include, path + from horilla.settings import MIDDLEWARE from horilla.urls import urlpatterns urlpatterns.append( path("attendance/", include("attendance.urls")), ) + middleware_path = "attendance.middleware.AttendanceMiddleware" + if middleware_path not in MIDDLEWARE: + MIDDLEWARE.append(middleware_path) + super().ready() diff --git a/attendance/middleware.py b/attendance/middleware.py new file mode 100644 index 000000000..8b9f05901 --- /dev/null +++ b/attendance/middleware.py @@ -0,0 +1,56 @@ +from django.utils.deprecation import MiddlewareMixin +from datetime import timedelta, datetime +from django.utils import timezone +from attendance.methods.utils import Request + + +class AttendanceMiddleware(MiddlewareMixin): + def process_request(self, request): + self.trigger_function() + + def trigger_function(self): + from base.models import EmployeeShiftSchedule + from attendance.models import AttendanceActivity, Attendance + from attendance.views.clock_in_out import clock_out + + automatic_check_out_shifts = EmployeeShiftSchedule.objects.filter( + is_auto_punch_out_enabled=True + ) + + for shift_schedule in automatic_check_out_shifts: + activities = AttendanceActivity.objects.filter( + shift_day=shift_schedule.day, + clock_out_date=None, + clock_out=None, + ).order_by("-created_at") + + for activity in activities: + attendance = Attendance.objects.filter( + employee_id=activity.employee_id, + attendance_clock_out=None, + attendance_clock_out_date=None, + shift_id=shift_schedule.shift_id, + attendance_day=shift_schedule.day, + attendance_date=activity.attendance_date, + ).first() + + if attendance: + date = activity.attendance_date + if shift_schedule.is_night_shift: + date += timedelta(days=1) + + combined_datetime = datetime.combine(date, shift_schedule.auto_punch_out_time) + current_time = timezone.now() + + if combined_datetime < current_time: + try: + clock_out( + Request( + user=attendance.employee_id.employee_user_id, + date=date, + time=shift_schedule.auto_punch_out_time, + datetime=combined_datetime, + ) + ) + except Exception as e: + print(f"{e}") diff --git a/base/forms.py b/base/forms.py index f8737452e..e33f51614 100644 --- a/base/forms.py +++ b/base/forms.py @@ -1054,25 +1054,70 @@ class EmployeeShiftScheduleUpdateForm(ModelForm): """ fields = "__all__" - exclude = ["is_active"] + exclude = ["is_active", "is_night_shift"] widgets = { "start_time": DateInput(attrs={"type": "time"}), "end_time": DateInput(attrs={"type": "time"}), + "auto_punch_out_time": DateInput(attrs={"type": "time"}), } model = EmployeeShiftSchedule def __init__(self, *args, **kwargs): if instance := kwargs.get("instance"): - # """ - # django forms not showing value inside the date, time html element. - # so here overriding default forms instance method to set initial value - # """ initial = { - "start_time": instance.start_time.strftime("%H:%M"), - "end_time": instance.end_time.strftime("%H:%M"), + "start_time": ( + instance.start_time.strftime("%H:%M") + if instance.start_time + else None + ), + "end_time": ( + instance.end_time.strftime("%H:%M") if instance.end_time else None + ), + "auto_punch_out_time": ( + instance.auto_punch_out_time.strftime("%H:%M") + if instance.auto_punch_out_time + else None + ), } kwargs["initial"] = initial super().__init__(*args, **kwargs) + self.fields["is_auto_punch_out_enabled"].widget.attrs.update( + {"onchange": "toggleDivVisibility(this)"} + ) + + def as_p(self): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + + context = {"form": self} + table_html = render_to_string("horilla_form.html", context) + return table_html + + def clean(self): + cleaned_data = super().clean() + auto_punch_out_enabled = self.cleaned_data["is_auto_punch_out_enabled"] + auto_punch_out_time = self.cleaned_data["auto_punch_out_time"] + end_time = self.cleaned_data["end_time"] + if auto_punch_out_enabled: + if not auto_punch_out_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time is required when automatic punch out is enabled." + ) + } + ) + if auto_punch_out_enabled and auto_punch_out_time and end_time: + if auto_punch_out_time < end_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time cannot be earlier than the end time." + ) + } + ) + return cleaned_data class EmployeeShiftScheduleForm(ModelForm): @@ -1095,6 +1140,7 @@ class EmployeeShiftScheduleForm(ModelForm): widgets = { "start_time": DateInput(attrs={"type": "time"}), "end_time": DateInput(attrs={"type": "time"}), + "auto_punch_out_time": DateInput(attrs={"type": "time"}), } def __init__(self, *args, **kwargs): @@ -1104,13 +1150,61 @@ class EmployeeShiftScheduleForm(ModelForm): # so here overriding default forms instance method to set initial value # """ initial = { - "start_time": instance.start_time.strftime("%H:%M"), - "end_time": instance.end_time.strftime("%H:%M"), + "start_time": ( + instance.start_time.strftime("%H:%M") + if instance.start_time + else None + ), + "end_time": ( + instance.end_time.strftime("%H:%M") if instance.end_time else None + ), + "auto_punch_out_time": ( + instance.auto_punch_out_time.strftime("%H:%M") + if instance.auto_punch_out_time + else None + ), } kwargs["initial"] = initial super().__init__(*args, **kwargs) self.fields["day"].widget.attrs.update({"id": str(uuid.uuid4())}) self.fields["shift_id"].widget.attrs.update({"id": str(uuid.uuid4())}) + self.fields["is_auto_punch_out_enabled"].widget.attrs.update( + {"onchange": "toggleDivVisibility(this)"} + ) + + def as_p(self): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + + context = {"form": self} + table_html = render_to_string("horilla_form.html", context) + return table_html + + def clean(self): + cleaned_data = super().clean() + auto_punch_out_enabled = self.cleaned_data["is_auto_punch_out_enabled"] + auto_punch_out_time = self.cleaned_data["auto_punch_out_time"] + end_time = self.cleaned_data["end_time"] + if auto_punch_out_enabled: + if not auto_punch_out_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time is required when automatic punch out is enabled." + ) + } + ) + if auto_punch_out_enabled and auto_punch_out_time and end_time: + if auto_punch_out_time < end_time: + raise ValidationError( + { + "auto_punch_out_time": _( + "Automatic punch out time cannot be earlier than the end time." + ) + } + ) + return cleaned_data def save(self, commit=True): instance = super().save(commit=False) diff --git a/base/models.py b/base/models.py index a2ac1ec23..31b5af4c4 100644 --- a/base/models.py +++ b/base/models.py @@ -570,17 +570,36 @@ class EmployeeShiftSchedule(HorillaModel): """ day = models.ForeignKey( - EmployeeShiftDay, on_delete=models.PROTECT, related_name="day_schedule" + EmployeeShiftDay, + on_delete=models.PROTECT, + related_name="day_schedule", + verbose_name=_("Shift Day"), ) shift_id = models.ForeignKey( EmployeeShift, on_delete=models.PROTECT, verbose_name=_("Shift") ) minimum_working_hour = models.CharField( - default="08:15", max_length=5, validators=[validate_time_format] + default="08:15", + max_length=5, + validators=[validate_time_format], + verbose_name=_("Minimum Working Hours"), + ) + start_time = models.TimeField(null=True, verbose_name=_("Start Time")) + end_time = models.TimeField(null=True, verbose_name=_("End Time")) + is_night_shift = models.BooleanField(default=False, verbose_name=_("Night Shift")) + is_auto_punch_out_enabled = models.BooleanField( + default=False, + verbose_name=_("Enable Automatic Check Out"), + help_text=_("Enable this to trigger automatic check out."), + ) + auto_punch_out_time = models.TimeField( + null=True, + blank=True, + verbose_name=_("Automatic Check Out Time"), + help_text=_( + "Time at which the horilla will automatically check out the employee attendance if they forget." + ), ) - start_time = models.TimeField(null=True) - end_time = models.TimeField(null=True) - is_night_shift = models.BooleanField(default=False) company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) objects = HorillaCompanyManager("shift_id__employee_shift__company_id") diff --git a/base/templates/base/shift/schedule.html b/base/templates/base/shift/schedule.html index 59fe952c2..c49efed64 100644 --- a/base/templates/base/shift/schedule.html +++ b/base/templates/base/shift/schedule.html @@ -3,21 +3,21 @@ {% block settings %}{% load static %}
{% endif %} {% endif %} - + {% endblock settings %} diff --git a/base/templates/base/shift/schedule_form.html b/base/templates/base/shift/schedule_form.html index 577c5e81b..54ea52427 100644 --- a/base/templates/base/shift/schedule_form.html +++ b/base/templates/base/shift/schedule_form.html @@ -1,33 +1,26 @@ {% load i18n %}