From 765f94bf1763299b60a9bd2b52f2f966dc478f97 Mon Sep 17 00:00:00 2001 From: Horilla Date: Fri, 9 Aug 2024 11:20:28 +0530 Subject: [PATCH] [UPDT] ATTENDANCE: Updated attendance activity page by adding option to import attendance activities in excel format --- attendance/methods/utils.py | 113 ++++- attendance/scheduler.py | 6 +- .../attendance_activity/activity_empty.html | 30 +- .../attendance_activity/activity_filters.html | 201 ++++---- .../attendance_activity/activity_list.html | 379 +++++++------- .../attendance_activity_view.html | 60 +-- .../attendance_activity/export_filter.html | 342 ++++++------- .../attendance_activity/group_by.html | 475 +++++++++--------- .../attendance_activity/import_activity.html | 78 +++ .../attendance/attendance_activity/nav.html | 290 ++++------- attendance/urls.py | 10 + attendance/views/clock_in_out.py | 96 ++-- attendance/views/views.py | 131 ++++- 13 files changed, 1198 insertions(+), 1013 deletions(-) create mode 100644 attendance/templates/attendance/attendance_activity/import_activity.html diff --git a/attendance/methods/utils.py b/attendance/methods/utils.py index e193e68c6..e9fd5ff95 100644 --- a/attendance/methods/utils.py +++ b/attendance/methods/utils.py @@ -5,8 +5,9 @@ This module is used write custom methods """ import calendar -from datetime import datetime, timedelta +from datetime import datetime, time, timedelta +import pandas as pd from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.db import models @@ -16,6 +17,7 @@ from django.utils.translation import gettext_lazy as _ from base.methods import get_pagination from base.models import WEEK_DAYS, CompanyLeaves, Holidays +from employee.models import Employee MONTH_MAPPING = { "january": 1, @@ -474,3 +476,112 @@ def validate_time_in_minutes(value): raise ValidationError(_("Invalid time, excepted MM:SS")) except ValueError as e: raise ValidationError(_("Invalid format, excepted MM:SS")) from e + + +class Request: + """ + Represents a request for clock-in or clock-out. + + Attributes: + - user: The user associated with the request. + - date: The date of the request. + - time: The time of the request. + - path: The path associated with the request (default: "/"). + - session: The session data associated with the request (default: {"title": None}). + """ + + def __init__( + self, + user, + date, + time, + datetime, + ) -> None: + self.user = user + self.path = "/" + self.session = {"title": None} + self.date = date + self.time = time + self.datetime = datetime + self.META = META() + + +class META: + """ + Provides access to HTTP metadata keys. + """ + + @classmethod + def keys(cls): + """ + Retrieve the list of available HTTP metadata keys. + + Returns: + list: A list of HTTP metadata keys. + """ + return ["HTTP_HX_REQUEST"] + + +def parse_time(time_str): + time_formats = { + "hh:mm A": "%I:%M %p", # 12-hour format + "HH:mm": "%H:%M", # 24-hour format + } + if isinstance(time_str, time): # Check if it's already a time object + return time_str + + if isinstance(time_str, str): + for format_str in time_formats.values(): + try: + return datetime.strptime(time_str, format_str).time() + except ValueError: + continue + return None + + +def parse_date(date_str, error_key, activity): + try: + return pd.to_datetime(date_str).date() + except (pd.errors.ParserError, ValueError): + activity[error_key] = f"Invalid date format for {error_key.split()[-1]}" + return None + + +def get_date(date): + date_formats = { + "DD-MM-YYYY": "%d-%m-%Y", + "DD.MM.YYYY": "%d.%m.%Y", + "DD/MM/YYYY": "%d/%m/%Y", + "MM/DD/YYYY": "%m/%d/%Y", + "YYYY-MM-DD": "%Y-%m-%d", + "YYYY/MM/DD": "%Y/%m/%d", + "MMMM D, YYYY": "%B %d, %Y", + "DD MMMM, YYYY": "%d %B, %Y", + "MMM. D, YYYY": "%b. %d, %Y", + "D MMM. YYYY": "%d %b. %Y", + "dddd, MMMM D, YYYY": "%A, %B %d, %Y", + } + if isinstance(date, datetime): + return date + elif isinstance(date, str): + for format_name, format_str in date_formats.items(): + try: + return datetime.strptime(date, format_str) + except ValueError: + continue + return None + + +def sort_activity_dicts(activity_dicts): + + for activity in activity_dicts: + activity["Attendance Date"] = get_date(activity["Attendance Date"]) + + # Filter out any entries where the date could not be parsed + activity_dicts = [ + activity + for activity in activity_dicts + if activity["Attendance Date"] is not None + ] + sorted_activity_dicts = sorted(activity_dicts, key=lambda x: x["Attendance Date"]) + return sorted_activity_dicts diff --git a/attendance/scheduler.py b/attendance/scheduler.py index 26384d85f..402ce5e0d 100644 --- a/attendance/scheduler.py +++ b/attendance/scheduler.py @@ -2,7 +2,7 @@ import datetime from datetime import datetime from apscheduler.schedulers.background import BackgroundScheduler -from django.utils import timezone +from django.utils import timezone as django_timezone def auto_check_out(): @@ -13,7 +13,7 @@ def auto_check_out(): from employee.models import Employee try: - today = datetime.now() + today = django_timezone.make_aware(datetime.now()) shift_schedules = EmployeeShiftSchedule.objects.all() employees = Employee.objects.all() for employee in employees: @@ -34,7 +34,7 @@ def auto_check_out(): not attendance_activity.clock_out and shift_schedule.start_time <= today.time() ): - clock_out_attendance_and_activity( + attendance = clock_out_attendance_and_activity( employee=employee, date_today=today.date(), now=today.time().strftime("%H:%M"), diff --git a/attendance/templates/attendance/attendance_activity/activity_empty.html b/attendance/templates/attendance/attendance_activity/activity_empty.html index c4a0ab89f..79e51797a 100644 --- a/attendance/templates/attendance/attendance_activity/activity_empty.html +++ b/attendance/templates/attendance/attendance_activity/activity_empty.html @@ -2,24 +2,20 @@ {% load static %} {% load i18n %}
-
-

{% trans "Attendance Activity" %}

-
+
+

{% trans "Attendance Activity" %}

+
-
-
- -
-
- Page not found. 404. -
{% trans "There are no attendance records to display." %}
-
-
-
+
+
+
+
+ Page not found. 404. +
{% trans "There are no attendance activity records to display." %}
+
+
+
-
- {% endblock %} diff --git a/attendance/templates/attendance/attendance_activity/activity_filters.html b/attendance/templates/attendance/attendance_activity/activity_filters.html index 0e217c7de..b68ba52ec 100644 --- a/attendance/templates/attendance/attendance_activity/activity_filters.html +++ b/attendance/templates/attendance/attendance_activity/activity_filters.html @@ -1,117 +1,114 @@ {% load static %} {% load i18n %} -
+
-
{% trans "Work Info" %}
-
-
-
-
- - {{f.form.employee_id}} +
{% trans "Work Info" %}
+
+
+
+
+ + {{f.form.employee_id}} +
+
+ + {{f.form.employee_id__employee_work_info__department_id}} +
+
+ + {{f.form.employee_id__employee_work_info__shift_id}} +
+
+ + {{f.form.employee_id__employee_work_info__reporting_manager_id}} +
+
+
+
+ + {{f.form.employee_id__employee_work_info__company_id}} +
+
+ + {{f.form.employee_id__employee_work_info__job_position_id}} +
+
+ + {{f.form.employee_id__employee_work_info__work_type_id}} +
+
+ + {{f.form.employee_id__employee_work_info__location}} +
+
-
- - {{f.form.employee_id__employee_work_info__department_id}} -
-
- - {{f.form.employee_id__employee_work_info__shift_id}} -
-
- - {{f.form.employee_id__employee_work_info__reporting_manager_id}} -
-
-
-
- - {{f.form.employee_id__employee_work_info__company_id}} -
-
- - {{f.form.employee_id__employee_work_info__job_position_id}} -
-
- - {{f.form.employee_id__employee_work_info__work_type_id}} -
-
- - {{f.form.employee_id__employee_work_info__location}} -
-
-
-
{% trans "Attendance Activity" %}
-
-
-
-
- - {{f.form.attendance_date}} +
{% trans "Attendance Activity" %}
+
+
+
+
+ + {{f.form.attendance_date}} +
+
+ + {{f.form.clock_out_date}} +
+
+
+
+ + {{f.form.clock_in_date}} +
+
+ + {{f.form.shift_day}} +
+
-
- - {{f.form.clock_out_date}} -
-
-
-
- - {{f.form.clock_in_date}} -
-
- - {{f.form.shift_day}} -
-
-
-
{% trans "Advanced" %}
-
-
-
-
- - {{f.form.attendance_date_from}} +
{% trans "Advanced" %}
+
+
+
+
+ + {{f.form.attendance_date_from}} +
+
+ + {{f.form.in_from}} +
+
+ + {{f.form.out_from}} +
+
+
+
+ + {{f.form.attendance_date_till}} +
+
+ + {{f.form.in_till}} +
+
+ + {{f.form.out_till}} +
+
-
- - {{f.form.in_from}} -
-
- - {{f.form.out_from}} -
-
-
-
- - {{f.form.attendance_date_till}} -
-
- - {{f.form.in_till}} -
-
- - {{f.form.out_till}} -
-
-
-
- + +
diff --git a/attendance/templates/attendance/attendance_activity/activity_list.html b/attendance/templates/attendance/attendance_activity/activity_list.html index f351a591e..11c98192e 100644 --- a/attendance/templates/attendance/attendance_activity/activity_list.html +++ b/attendance/templates/attendance/attendance_activity/activity_list.html @@ -1,194 +1,227 @@ - {% load i18n %} {% load static %} {% include 'filter_tags.html' %} + {% if messages %} -
- {% for message in messages %} -
-
- {{ message }} +
+ {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %}
-
- {% endfor %} -
{% endif %} + {% if data %} -
-
-
- -
-
    -
-
-
-
-
-
-
-
-
-
-
-
- + +
+
+
+ +
+
    +
-
-
{% trans "Employee" %}
-
{% trans "Attendance Date" %}
-
{% trans "In Date" %}
-
{% trans "Check In" %}
-
{% trans "Check Out" %}
-
{% trans "Out Date" %}
- {% if perms.attendance.delete_attendanceactivity %} -
{% trans "Actions" %}
- {% endif %}
-
-
- {% for activity in data %} -
-
-
- -
-
-
-
-
- -
- {{activity.employee_id}} -
-
-
{{activity.attendance_date}}
-
{{activity.clock_in_date}}
-
{{activity.clock_in}}
-
{{activity.clock_out}}
-
{{activity.clock_out_date}}
- {% if perms.attendance.delete_attendanceactivity %} -
-
- {% csrf_token %} - -
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ {% trans "Employee" %} +
+
+ {% trans "Attendance Date" %} +
+
+ {% trans "In Date" %} +
+
{% trans "Check In" %}
+
{% trans "Check Out" %}
+
+ {% trans "Out Date" %} +
+ {% if perms.attendance.delete_attendanceactivity %} +
{% trans "Actions" %}
+ {% endif %}
- {% endif %} +
+
+ {% for activity in data %} +
+
+
+ +
+
+
+
+
+ +
+ {{activity.employee_id}} +
+
+
{{activity.attendance_date}}
+
{{activity.clock_in_date}}
+
{{activity.clock_in}}
+
{{activity.clock_out}}
+
{{activity.clock_out_date}}
+ {% if perms.attendance.delete_attendanceactivity %} +
+
+ {% csrf_token %} + +
+
+ {% endif %} +
+ {% endfor %} +
- {% endfor %} +
+
+ + {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. + +
-
-
- - {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. - - -
-
+ {% else %} - -
- -
- {% trans "No search result found!" %} -
-
- + +
+ +
+ {% trans "No search result found!" %} +
+
+ {% endif %} diff --git a/attendance/templates/attendance/attendance_activity/attendance_activity_view.html b/attendance/templates/attendance/attendance_activity/attendance_activity_view.html index 784ff08ac..b482c266a 100644 --- a/attendance/templates/attendance/attendance_activity/attendance_activity_view.html +++ b/attendance/templates/attendance/attendance_activity/attendance_activity_view.html @@ -1,47 +1,27 @@ {% extends 'index.html' %}{% block content %} {% load i18n %}{% load static %} {% load basefilters %}
- {% include 'attendance/attendance_activity/nav.html' %} - -
- {% if perms.attendance.change_attendancelatecomeearlyout or request.user|is_reportingmanager %} -
- {% trans "Select All Attendance" %} + {% include 'attendance/attendance_activity/nav.html' %} + -
{% endblock %} diff --git a/attendance/templates/attendance/attendance_activity/export_filter.html b/attendance/templates/attendance/attendance_activity/export_filter.html index f39be8b7a..5c43be942 100644 --- a/attendance/templates/attendance/attendance_activity/export_filter.html +++ b/attendance/templates/attendance/attendance_activity/export_filter.html @@ -1,195 +1,151 @@ {% load static %} {% load i18n %} -
-
-
{% trans "Excel columns" %}
-
-
-
-
- -
-
-
-
- {% for field in export_form.selected_fields %} -
-
- -
-
- {% endfor %} -
-
-
-
-
{% trans "Work Info" %}
-
-
-
-
- - {{export.form.employee_id}} -
-
- - {{export.form.employee_id__employee_work_info__department_id}} -
-
- - {{export.form.employee_id__employee_work_info__shift_id}} -
-
- - {{export.form.employee_id__employee_work_info__reporting_manager_id}} -
-
-
-
- - {{export.form.employee_id__employee_work_info__company_id}} -
-
- - {{export.form.employee_id__employee_work_info__job_position_id}} -
-
- - {{export.form.employee_id__employee_work_info__work_type_id}} -
-
- - {{export.form.employee_id__employee_work_info__location}} -
-
-
-
-
-
-
{% trans "Attendance Activity" %}
-
-
-
-
- - {{export.form.attendance_date}} -
-
- - {{export.form.clock_out_date}} -
-
-
-
- - {{export.form.clock_in_date}} -
-
- - {{export.form.shift_day}} -
-
-
-
-
-
-
{% trans "Advanced" %}
-
-
-
-
- - {{export.form.attendance_date_from}} -
-
- - {{export.form.in_from}} -
-
- - {{export.form.out_from}} -
-
-
-
- - {{export.form.attendance_date_till}} -
-
- - {{export.form.in_till}} -
-
- - {{export.form.out_till}} -
-
-
-
-
+
+

+ {% trans "Export Attendance Activities" %} +

+ +
+
+
+ {% csrf_token %} +
+
+
{% trans "Excel columns" %}
+
+
+
+
+ +
+
+
+
+ {% for field in export_form.selected_fields %} +
+
+ +
+
+ {% endfor %} +
+
+
+
+
{% trans "Work Info" %}
+
+
+
+
+ + {{export.form.employee_id}} +
+
+ + {{export.form.employee_id__employee_work_info__department_id}} +
+
+ + {{export.form.employee_id__employee_work_info__shift_id}} +
+
+ + {{export.form.employee_id__employee_work_info__reporting_manager_id}} +
+
+
+
+ + {{export.form.employee_id__employee_work_info__company_id}} +
+
+ + {{export.form.employee_id__employee_work_info__job_position_id}} +
+
+ + {{export.form.employee_id__employee_work_info__work_type_id}} +
+
+ + {{export.form.employee_id__employee_work_info__location}} +
+
+
+
+
+
+
{% trans "Attendance Activity" %}
+
+
+
+
+ + {{export.form.attendance_date}} +
+
+ + {{export.form.clock_out_date}} +
+
+
+
+ + {{export.form.clock_in_date}} +
+
+ + {{export.form.shift_day}} +
+
+
+
+
+
+
{% trans "Advanced" %}
+
+
+
+
+ + {{export.form.attendance_date_from}} +
+
+ + {{export.form.in_from}} +
+
+ + {{export.form.out_from}} +
+
+
+
+ + {{export.form.attendance_date_till}} +
+
+ + {{export.form.in_till}} +
+
+ + {{export.form.out_till}} +
+
+
+
+
+
+ +
diff --git a/attendance/templates/attendance/attendance_activity/group_by.html b/attendance/templates/attendance/attendance_activity/group_by.html index 21ee5bf2e..0ec4843f5 100644 --- a/attendance/templates/attendance/attendance_activity/group_by.html +++ b/attendance/templates/attendance/attendance_activity/group_by.html @@ -2,268 +2,253 @@ {% load static %} {% load attendancefilters %} {% include 'filter_tags.html' %} + {% if messages %} -
- {% for message in messages %} -
-
- {{ message }} +
+ {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %}
-
- {% endfor %} -
{% endif %} {% if data %} -
-{% for attendance_list in data %} -
-
-
- -
- - {{attendance_list.list.paginator.count}} - - {{attendance_list.grouper}} -
-
-
-
-
-
-
-
-
-
-
- -
+
+ {% for attendance_list in data %} +
+
+
+ +
+ + {{attendance_list.list.paginator.count}} + + {{attendance_list.grouper}} +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ {% trans "Employee" %} +
+
+ {% trans "Attendnace Date" %} +
+
+ {% trans "In Date" %} +
+
{% trans "Check In" %}
+
{% trans "Check Out" %}
+
+ {% trans "Out Date" %} +
+ {% if perms.attendance.delete_attendanceactivity %} +
{% trans "Actions" %}
+ {% endif %} +
+
+
+ {% for activity in attendance_list.list %} +
+
+
+ +
+
+
+
+
+ +
+ {{activity.employee_id}} +
+
+
{{activity.attendance_date}} +
+
{{activity.clock_in_date}}
+
{{activity.clock_in}}
+
{{activity.clock_out}}
+
{{activity.clock_out_date}} +
+ {% if perms.attendance.delete_attendanceactivity %} +
+
+ {% csrf_token %} + +
+
+ {% endif %} +
+ {% endfor %} +
+
+
+
+ + {% trans "Page" %} {{ attendance_list.list.number }} {% trans "of" %} {{ attendance_list.list.paginator.num_pages }}. + + +
+
+
-
{% trans "Employee" %}
-
{% trans "Attendnace Date" %}
-
{% trans "In Date" %}
-
{% trans "Check In" %}
-
{% trans "Check Out" %}
-
{% trans "Out Date" %}
- {% if perms.attendance.delete_attendanceactivity %} -
{% trans "Actions" %}
- {% endif %} -
-
- {% for activity in attendance_list.list %} -
-
-
- -
-
-
-
-
- -
- {{activity.employee_id}} -
-
-
{{activity.attendance_date}}
-
{{activity.clock_in_date}}
-
{{activity.clock_in}}
-
{{activity.clock_out}}
-
{{activity.clock_out_date}}
- {% if perms.attendance.delete_attendanceactivity %} -
-
- {% csrf_token %} - -
-
+ {% endfor %} +
+ + {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. + +
- {% endfor %} -
-
-
-
- - {% trans "Page" %} {{ attendance_list.list.number }} {% trans "of" %} {{ attendance_list.list.paginator.num_pages }}. - - + {% if data.has_next %} +
  • + {% trans "Next" %} +
  • +
  • + {% trans "Last" %} +
  • + {% endif %} + +
    -
    -
    -{% endfor %} -
    -
    - - {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. - - -
    -
    {% else %} -
    -
    +
    +
    -
    -
    - Page not found. 404. -
    {% trans "No group result found!" %}
    -
    -
    -
    -
    +
    +
    + Page not found. 404. +
    {% trans "No group result found!" %}
    +
    +
    +
    +
    {% endif %} diff --git a/attendance/templates/attendance/attendance_activity/import_activity.html b/attendance/templates/attendance/attendance_activity/import_activity.html new file mode 100644 index 000000000..0444f07a5 --- /dev/null +++ b/attendance/templates/attendance/attendance_activity/import_activity.html @@ -0,0 +1,78 @@ +{% load i18n %} +
    +

    + {% trans "Import Attendance Activities" %} +

    + + +
    + +
    +
    + {% csrf_token %} + + +
    + + +
    + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/attendance/templates/attendance/attendance_activity/nav.html b/attendance/templates/attendance/attendance_activity/nav.html index b42b8d23f..8fe236358 100644 --- a/attendance/templates/attendance/attendance_activity/nav.html +++ b/attendance/templates/attendance/attendance_activity/nav.html @@ -1,191 +1,109 @@ {% load i18n %} {% load basefilters %}
    - - -
    -
    -
    - - -
    - -
    -
    - - -
    -
    -
    - - -
    - {% if perms.attendance.change_attendancelatecomeearlyout or request.user|is_reportingmanager or perms.attendance.delete_attendancelatecomeearlyout %} -
    - - -
    - {% endif %}
    -
    - +
    +
    +
    + + +
    + +
    +
    + + +
    +
    + + +
    + {% if perms.attendance.change_attendancelatecomeearlyout or request.user|is_reportingmanager or perms.attendance.delete_attendancelatecomeearlyout %} +
    + + +
    + {% endif %} +
    +
    +
    +
    diff --git a/attendance/urls.py b/attendance/urls.py index 4b43e7ad8..c20abb753 100644 --- a/attendance/urls.py +++ b/attendance/urls.py @@ -115,6 +115,16 @@ urlpatterns = [ views.attendance_activity_bulk_delete, name="attendance-activity-bulk-delete", ), + path( + "attendance-activity-import", + views.attendance_activity_import, + name="attendance-activity-import", + ), + path( + "attendance-activity-import-excel", + views.attendance_activity_import_excel, + name="attendance-activity-import-excel", + ), path( "attendance-activity-info-export", views.attendance_activity_export, diff --git a/attendance/views/clock_in_out.py b/attendance/views/clock_in_out.py index 2e9f7efea..9d107f58e 100644 --- a/attendance/views/clock_in_out.py +++ b/attendance/views/clock_in_out.py @@ -4,6 +4,9 @@ clock_in_out.py This module is used register endpoints to the check-in check-out functionalities """ +import logging + +logger = logging.getLogger(__name__) from datetime import date, datetime, timedelta from django.db.models import Q @@ -145,15 +148,14 @@ def clock_in_attendance_and_activity( activity.clock_out_date = date_today activity.save() - AttendanceActivity( + new_activity = AttendanceActivity.objects.create( employee_id=employee, attendance_date=attendance_date, clock_in_date=date_today, shift_day=day, clock_in=in_datetime, in_datetime=in_datetime, - ).save() - + ) # create attendance if not exist attendance = Attendance.objects.filter( employee_id=employee, attendance_date=attendance_date @@ -248,7 +250,7 @@ def clock_in(request): ) attendance_date = date_yesterday day = day_yesterday - clock_in_attendance_and_activity( + attendance = clock_in_attendance_and_activity( employee=employee, date_today=date_today, attendance_date=attendance_date, @@ -324,6 +326,7 @@ def clock_out_attendance_and_activity(employee, date_today, now, out_datetime=No attendance_activities = AttendanceActivity.objects.filter( employee_id=employee, ).order_by("attendance_date", "id") + attendance_activity = None # Initialize attendance_activity if attendance_activities.filter(clock_out__isnull=True).exists(): attendance_activity = attendance_activities.filter( @@ -334,34 +337,37 @@ def clock_out_attendance_and_activity(employee, date_today, now, out_datetime=No attendance_activity.out_datetime = out_datetime attendance_activity.save() - attendance_activities = attendance_activities.filter( - attendance_date=attendance_activity.attendance_date - ) - # Here calculate the total durations between the attendance activities + attendance_activities = attendance_activities.filter( + attendance_date=attendance_activity.attendance_date + ) + # Here calculate the total durations between the attendance activities - duration = 0 - for attendance_activity in attendance_activities: - in_datetime, out_datetime = activity_datetime(attendance_activity) - difference = out_datetime - in_datetime - days_second = difference.days * 24 * 3600 - seconds = difference.seconds - total_seconds = days_second + seconds - duration = duration + total_seconds - duration = format_time(duration) - # update clock out of attendance - attendance = Attendance.objects.filter(employee_id=employee).order_by( - "-attendance_date", "-id" - )[0] - attendance.attendance_clock_out = now + ":00" - attendance.attendance_clock_out_date = date_today - attendance.attendance_worked_hour = duration - # Overtime calculation - attendance.attendance_overtime = overtime_calculation(attendance) + duration = 0 + for activity in attendance_activities: + in_datetime, out_datetime = activity_datetime(activity) + difference = out_datetime - in_datetime + days_second = difference.days * 24 * 3600 + seconds = difference.seconds + total_seconds = days_second + seconds + duration = duration + total_seconds + duration = format_time(duration) + # update clock out of attendance + attendance = Attendance.objects.filter(employee_id=employee).order_by( + "-attendance_date", "-id" + )[0] + attendance.attendance_clock_out = now + ":00" + attendance.attendance_clock_out_date = date_today + attendance.attendance_worked_hour = duration + # Overtime calculation + attendance.attendance_overtime = overtime_calculation(attendance) - # Validate the attendance as per the condition - attendance.attendance_validated = attendance_validate(attendance) - attendance.save() + # Validate the attendance as per the condition + attendance.attendance_validated = attendance_validate(attendance) + attendance.save() + return attendance + + logger.error("No attendance clock in activity found that needs clocking out.") return @@ -396,7 +402,12 @@ def early_out(attendance, start_time, end_time, shift): """ if not enable_late_come_early_out_tracking(None).get("tracking"): return - now_sec = strtime_seconds(attendance.attendance_clock_out.strftime("%H:%M")) + + clock_out_time = attendance.attendance_clock_out + if isinstance(clock_out_time, str): + clock_out_time = datetime.strptime(clock_out_time, "%H:%M:%S") + + now_sec = strtime_seconds(clock_out_time.strftime("%H:%M")) mid_day_sec = strtime_seconds("12:00") # Checking gracetime allowance before creating early out if shift and shift.grace_time_id: @@ -458,23 +469,18 @@ def clock_out(request): minimum_hour, start_time_sec, end_time_sec = shift_schedule_today( day=day, shift=shift ) - - clock_out_attendance_and_activity( + attendance = clock_out_attendance_and_activity( employee=employee, date_today=date_today, now=now, out_datetime=datetime_now ) - attendance = ( - Attendance.objects.filter(employee_id=employee) - .order_by("id", "attendance_date") - .last() - ) - early_out_instance = attendance.late_come_early_out.filter(type="early_out") - if not early_out_instance.exists(): - early_out( - attendance=attendance, - start_time=start_time_sec, - end_time=end_time_sec, - shift=shift, - ) + if attendance: + early_out_instance = attendance.late_come_early_out.filter(type="early_out") + if not early_out_instance.exists(): + early_out( + attendance=attendance, + start_time=start_time_sec, + end_time=end_time_sec, + shift=shift, + ) script = "" hidden_label = "" diff --git a/attendance/views/views.py b/attendance/views/views.py index 16bfa2626..c522a0615 100644 --- a/attendance/views/views.py +++ b/attendance/views/views.py @@ -11,6 +11,10 @@ This module is part of the recruitment project and is intended to provide the main entry points for interacting with the application's functionality. """ +import logging + +logger = logging.getLogger(__name__) + import calendar import contextlib import io @@ -28,6 +32,7 @@ from django.forms import ValidationError from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import redirect, render from django.urls import reverse +from django.utils import timezone as django_timezone from django.utils.translation import gettext as __ from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods @@ -55,11 +60,15 @@ from attendance.forms import ( LateComeEarlyOutExportForm, ) from attendance.methods.utils import ( + Request, attendance_day_checking, format_time, is_reportingmanger, monthly_leave_days, paginator_qry, + parse_date, + parse_time, + sort_activity_dicts, strtime_seconds, ) from attendance.models import ( @@ -89,11 +98,8 @@ from base.methods import ( get_key_instances, ) from base.models import ( - WEEK_DAYS, AttendanceAllowedIP, - CompanyLeaves, EmployeeShiftSchedule, - Holidays, TrackLateComeEarlyOut, ) from employee.filters import EmployeeFilter @@ -211,6 +217,8 @@ def attendance_create(request): return render(request, "attendance/attendance/form.html", {"form": form}) +@login_required +@permission_required("attendance.add_attendance") def attendance_excel(_request): """ Generate an empty Excel template for attendance data with predefined columns. @@ -745,7 +753,6 @@ def attendance_activity_view(request): activity_ids = json.dumps( [instance.id for instance in paginator_qry(attendance_activities, None)] ) - export_form = AttendanceActivityExportForm() if attendance_activities.exists(): template = "attendance/attendance_activity/attendance_activity_view.html" else: @@ -757,11 +764,7 @@ def attendance_activity_view(request): "data": paginator_qry(attendance_activities, request.GET.get("page")), "pd": previous_data, "f": filter_obj, - "export": AttendanceActivityFilter( - queryset=AttendanceActivity.objects.all() - ), "gp_fields": AttendanceActivityReGroup.fields, - "export_form": export_form, "activity_ids": activity_ids, }, ) @@ -854,9 +857,121 @@ def attendance_activity_bulk_delete(request): return JsonResponse({"message": "Success"}) +def process_activity_dicts(activity_dicts): + from attendance.views.clock_in_out import clock_in, clock_out + + if not activity_dicts: + return + sorted_activity_dicts = sort_activity_dicts(activity_dicts) + for activity in sorted_activity_dicts: + badge_id = activity.get("Badge ID") + if not badge_id: + activity["Error 1"] = "Please add the Badge ID column in the Excel sheet." + continue + + employee = Employee.objects.filter(badge_id=badge_id).first() + if not employee: + activity["Error 2"] = "Invalid Badge ID" + continue + + check_in_date = parse_date(activity["In Date"], "Error 4", activity) + check_out_date = parse_date(activity["Out Date"], "Error 5", activity) + check_in_time = ( + parse_time(activity["Check In"]) + if not pd.isna(activity["Check In"]) + else None + ) + check_out_time = ( + parse_time(activity["Check Out"]) + if not pd.isna(activity["Check Out"]) + else None + ) + if not any(key.startswith("Error") for key in activity.keys()): + if check_in_time: + try: + clock_in( + Request( + user=employee.employee_user_id, + date=check_in_date, + time=check_in_time, + datetime=django_timezone.make_aware( + datetime.combine(check_in_date, check_in_time) + ), + ) + ) + except Exception as e: + logger.error(f"Got an error in import clock in {e}") + + if check_out_time and check_out_date: + try: + clock_out( + Request( + user=employee.employee_user_id, + date=check_out_date, + time=check_out_time, + datetime=django_timezone.make_aware( + datetime.combine(check_out_date, check_out_time) + ), + ) + ) + except Exception as e: + logger.error(f"Got an error in import clock out {e}") + return activity_dicts + + +@login_required +@permission_required("attendance.add_attendanceactivity") +def attendance_activity_import(request): + if request.method == "POST": + file = request.FILES["activity_import"] + data_frame = pd.read_excel(file) + activity_dicts = data_frame.to_dict("records") + if activity_dicts: + activity_dicts = process_activity_dicts(activity_dicts) + messages.success(request, _("Attendance activity imported successfully")) + return redirect(attendance_activity_view) + return render(request, "attendance/attendance_activity/import_activity.html") + + +@login_required +@permission_required("attendance.add_attendanceactivity") +def attendance_activity_import_excel(request): + if request.method == "GET": + data_frame = pd.DataFrame( + columns=[ + "Badge ID", + "Employee", + "Attendance Date", + "In Date", + "Check In", + "Check Out", + "Out Date", + ] + ) + response = HttpResponse( + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + response["Content-Disposition"] = 'attachment; filename="activity_excel.xlsx"' + data_frame.to_excel(response, index=False) + return response + + @login_required @permission_required("attendance.change_attendanceactivity") def attendance_activity_export(request): + if request.META.get("HTTP_HX_REQUEST") == "true": + export_form = AttendanceActivityExportForm() + context = { + "export_form": export_form, + "export": AttendanceActivityFilter( + queryset=AttendanceActivity.objects.all() + ), + } + return render( + request, + "attendance/attendance_activity/export_filter.html", + context=context, + ) return export_data( request=request, model=AttendanceActivity,