diff --git a/biometric/forms.py b/biometric/forms.py index 0deca15f8..c457a7af8 100644 --- a/biometric/forms.py +++ b/biometric/forms.py @@ -39,6 +39,8 @@ class BiometricDeviceForm(ModelForm): exclude = [ "is_scheduler", "scheduler_duration", + "last_fetch_date", + "last_fetch_time", "is_active", ] widgets = { diff --git a/biometric/models.py b/biometric/models.py index 99311813a..686df7e16 100644 --- a/biometric/models.py +++ b/biometric/models.py @@ -54,6 +54,12 @@ class BiometricDevices(HorillaModel): ("dahua", _("Dahua Biometric")), ("etimeoffice", _("e-Time Office")), ] + BIO_DEVICE_DIRECTION = [ + ("in", _("In Device")), + ("out", _("Out Device")), + ("alternate", _("Alternate In/Out Device")), + ("system", _("System Direction(In/Out) Device")), + ] id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) name = models.CharField(max_length=100, verbose_name=_("Name")) machine_type = models.CharField( @@ -96,6 +102,12 @@ class BiometricDevices(HorillaModel): ) last_fetch_date = models.DateField(null=True, blank=True) last_fetch_time = models.TimeField(null=True, blank=True) + device_direction = models.CharField( + max_length=50, + choices=BIO_DEVICE_DIRECTION, + default="system", + verbose_name=_("Device Direction"), + ) company_id = models.ForeignKey( Company, null=True, diff --git a/biometric/templates/biometric/add_biometric_device.html b/biometric/templates/biometric/add_biometric_device.html deleted file mode 100644 index eb176a944..000000000 --- a/biometric/templates/biometric/add_biometric_device.html +++ /dev/null @@ -1,158 +0,0 @@ -{% load static %} {% load i18n %} -{% if messages %} - -{% endif %} -
-

- {% trans "Add" %} {{biometric_form.verbose_name}} -

- -
- -
-
- {% csrf_token %} -
-
- - {{biometric_form.name}} - {% if biometric_form.name.errors %} - {{ biometric_form.name.errors }} - {% endif %} -
-
- - {{biometric_form.machine_type}} - {% if biometric_form.machine_type.errors %} - {{ biometric_form.machine_type.errors }} - {% endif %} -
- - - - - - - - - -
- - {{biometric_form.company_id}} - {% if biometric_form.company_id.errors %} - {{ biometric_form.company_id.errors }} - {% endif %} -
-
- -
-
- diff --git a/biometric/templates/biometric/biometric_device_filter.html b/biometric/templates/biometric/biometric_device_filter.html index c135ac3fe..5d27e22e4 100644 --- a/biometric/templates/biometric/biometric_device_filter.html +++ b/biometric/templates/biometric/biometric_device_filter.html @@ -6,22 +6,22 @@
- - {{f.form.machine_type}} + + {{ f.form.machine_type }}
- - {{f.form.is_scheduler}} + + {{ f.form.is_scheduler }}
- - {{f.form.is_active}} + + {{ f.form.is_active }}
- - {{f.form.is_live}} + + {{ f.form.is_live }}
@@ -33,4 +33,3 @@ {% trans "Filter" %} - diff --git a/biometric/templates/biometric/biometric_device_form.html b/biometric/templates/biometric/biometric_device_form.html new file mode 100644 index 000000000..f96613783 --- /dev/null +++ b/biometric/templates/biometric/biometric_device_form.html @@ -0,0 +1,178 @@ +{% load static %} {% load i18n %} {% if messages %} + + +{% endif %} + +
+

+ {% if device_id %} {% trans "Edit" %} {% else %} {% trans "Add" %} {% endif %} {{ biometric_form.verbose_name }} +

+ +
+ +
+
+ {% csrf_token %} {{ biometric_form.non_field_errors }} +
+ {# name #} +
+ + {{ biometric_form.name }} + {% if biometric_form.name.errors %} + {{ biometric_form.name.errors }} + {% endif %} +
+ + {# machine_type #} +
+ + {{ biometric_form.machine_type }} + {% if biometric_form.machine_type.errors %} + {{ biometric_form.machine_type.errors }} + {% endif %} +
+ + {# machine_ip #} + + + {# port #} + + + {# zk_password #} + + + {# bio_username #} + + + {# bio_password #} + + + {# Anviz specific fields #} + + + + + + + + + {# direction and company #} +
+ + {{ biometric_form.device_direction }} + {% if biometric_form.device_direction.errors %} + {{ biometric_form.device_direction.errors }} + {% endif %} +
+ +
+ + {{ biometric_form.company_id }} + {% if biometric_form.company_id.errors %} + {{ biometric_form.company_id.errors }} + {% endif %} +
+
+ + +
+
+ + \ No newline at end of file diff --git a/biometric/templates/biometric/card_biometric_devices.html b/biometric/templates/biometric/card_biometric_devices.html index c8876d874..9e5c3161d 100644 --- a/biometric/templates/biometric/card_biometric_devices.html +++ b/biometric/templates/biometric/card_biometric_devices.html @@ -1,6 +1,4 @@ -{% load static %} -{% load i18n %} -{% load basefilters %} +{% load static %} {% load i18n %} {% load basefilters %} {% include 'filter_tags.html' %} {% if messages %} @@ -20,34 +18,30 @@ {% for device in devices %}
- {{device.name}} - {{device.get_machine_type_display}} + {{ device.name }} + {{ device.get_machine_type_display }} + {{ device.get_device_direction_display }} {% if device.machine_type == "zk" or device.machine_type == "cosec" or device.machine_type == "dahua" %} - {{device.machine_ip}} - {{device.port}} + {{ device.machine_ip }} + {{ device.port }} {% else %} - {{device.api_url}} + {{ device.api_url }} {% endif %} - {% if device.machine_type == "zk" or device.machine_type == "cosec" %} - - + {% if device.machine_type == "zk" or device.machine_type == "cosec" %} + + {% endif %}
- {% trans "Activate live capture mode" %} - -
- -
-
+ {% trans "Activate live capture mode" %} + +
+ +
+
@@ -66,71 +60,76 @@ hx-target="#objectUpdateModalTarget">{% trans "Edit" %}
  • - {% trans "Fetch Logs" %} + + {% trans "Fetch Logs" %}
  • {% if perms.biometric.change_biometricdevices %} {% if device.is_active %}
  • - {% trans "Archive" %} + {% trans "Archive" %} +
  • {% else %} -
  • - {% trans "Un-Archive" %} -
  • +
  • + {% trans "Un-Archive" %} + +
  • {% endif %} {% endif %} {% if perms.biometric.delete_biometricdevices %}
  • -
    - {% csrf_token %} - -
    +
  • {% endif %}
    -
    +
    {% trans "Test" %} {% if device.is_scheduler %} - {% trans "Unschedule" %} {% else %} - {% trans "Schedule" %} + {% trans "Schedule" %} + {% endif %} {% if device.machine_type == "zk" or device.machine_type == "cosec" or device.machine_type == "dahua" or device.machine_type == "etimeoffice" %} {% trans "Employee" %} + class="oh-checkpoint-badge text-secondary bio-user-list">{% trans "Employee" %} + {% endif %}
    - +
    {% endfor %} -
    +
    {% trans "Page" %} {{ devices.number }} {% trans "of" %} {{ devices.paginator.num_pages }}. @@ -138,33 +137,38 @@
    {% trans "Page" %} - - {% trans "of" %} {{devices.paginator.num_pages}} + + {% trans "of" %} {{ devices.paginator.num_pages }}
    @@ -190,11 +194,11 @@
    {% else %} -
    -
    - Page not found. 404. -

    {% trans "No Records found." %}

    -

    {% trans "No biometric devices found." %}

    -
    +
    +
    + Page not found. 404. +

    {% trans "No Records found." %}

    +

    {% trans "No biometric devices found." %}

    +
    {% endif %} diff --git a/biometric/templates/biometric/edit_biometric_device.html b/biometric/templates/biometric/edit_biometric_device.html deleted file mode 100644 index 06f390ef8..000000000 --- a/biometric/templates/biometric/edit_biometric_device.html +++ /dev/null @@ -1,140 +0,0 @@ -{% load i18n %} -{% if messages %} - -{% endif %} -{% if device_id and biometric_form %} -
    -

    - {% trans "Edit" %} {{biometric_form.verbose_name}} -

    - -
    -
    -
    - {% csrf_token %} {{form.errors}} {{biometric_form.non_field_errors}} -
    -
    - - {{biometric_form.name}} - {% if biometric_form.name.errors %} - {{biometric_form.name.errors}} - {% endif %} -
    -
    - - {{biometric_form.machine_type}} - {% if biometric_form.machine_type.errors %} - {{biometric_form.machine_type.errors}} - {% endif %} -
    - - - - - - - - - -
    - - {{biometric_form.company_id}} - {% if biometric_form.company_id.errors %} - {{biometric_form.company_id.errors}} - {% endif %} -
    -
    - -
    -
    - -{% endif %} diff --git a/biometric/templates/biometric/nav_biometric_devices.html b/biometric/templates/biometric/nav_biometric_devices.html index 1fc35013e..3949b766f 100644 --- a/biometric/templates/biometric/nav_biometric_devices.html +++ b/biometric/templates/biometric/nav_biometric_devices.html @@ -21,8 +21,9 @@
    + name="search" placeholder="{% trans 'Search' %}" hx-get="{% url 'view-biometric-devices' %}" + hx-swap="outerHTML" hx-select="#biometricDeviceList" hx-target="#biometricDeviceList" + hx-trigger="keyup delay:500ms" />
    @@ -33,8 +34,7 @@ diff --git a/biometric/urls.py b/biometric/urls.py index c2ebc3298..709364c4d 100644 --- a/biometric/urls.py +++ b/biometric/urls.py @@ -72,11 +72,6 @@ urlpatterns = [ views.biometric_device_archive, name="biometric-device-archive", ), - path( - "search-devices", - views.search_devices, - name="search-devices", - ), path( "biometric-device-employees//", views.biometric_device_employees, diff --git a/biometric/views.py b/biometric/views.py index 26789d4d0..9c26a5a3f 100644 --- a/biometric/views.py +++ b/biometric/views.py @@ -332,36 +332,50 @@ class COSECBioAttendanceThread(Thread): @permission_required("biometric.view_biometricdevices") def biometric_devices_view(request): """ - Renders a page displaying a list of active biometric devices. + Renders and filters the list of biometric devices based on query parameters. - Parameters: - - request: The HTTP request object. - - Returns: - - HttpResponse: The rendered HTML page displaying the list of biometric devices. + Handles both initial page load and HTMX-based filter/search requests. Template: - "biometric/view_biometric_devices.html" Context: - biometric_form (BiometricDeviceForm): Form for adding new biometric devices. - - devices (QuerySet): Queryset of active biometric devices, ordered by creation date. - - f (BiometricDeviceFilter): Form for filtering biometric devices. - + - devices (QuerySet): Filtered and paginated queryset of biometric devices. + - f (BiometricDeviceFilter): Filter form. + - pd (str): URL-encoded query params for HTMX push. + - filter_dict (dict): Parsed filter query params. """ - biometric_form = BiometricDeviceForm() - filter_form = BiometricDeviceFilter() - biometric_devices = BiometricDevices.objects.filter(is_active=True).order_by( - "-created_at" - ) + previous_data = request.GET.urlencode() + is_active = request.GET.get("is_active") + + # Apply filters + filter_form = BiometricDeviceFilter(request.GET) + biometric_devices = filter_form.qs.order_by("-created_at") + + # Default to is_active=True if not specified or "unknown" + if not is_active or is_active == "unknown": + biometric_devices = biometric_devices.filter(is_active=True) + + # Paginate biometric_devices = paginator_qry(biometric_devices, request.GET.get("page")) - template = "biometric/view_biometric_devices.html" - context = { - "biometric_form": biometric_form, - "devices": biometric_devices, - "f": filter_form, - } - return render(request, template, context) + + # Parse filters for reuse + data_dict = parse_qs(previous_data) + get_key_instances(BiometricDevices, data_dict) + + # Render + return render( + request, + "biometric/view_biometric_devices.html", + { + "biometric_form": BiometricDeviceForm(), + "devices": biometric_devices, + "f": filter_form, + "pd": previous_data, + "filter_dict": data_dict, + }, + ) @login_required @@ -528,7 +542,7 @@ def biometric_device_unschedule(request, device_id): device.is_scheduler = False device.save() messages.success(request, _("Biometric device unscheduled successfully")) - return redirect(f"/biometric/search-devices?{previous_data}") + return redirect(f"/biometric/view-biometric-devices/?{previous_data}") @login_required @@ -545,7 +559,10 @@ def biometric_device_add(request): Returns: - HttpResponse: Renders the 'add_biometric_device.html' template with the biometric device form. """ - previous_data = unquote(request.GET.urlencode())[len("pd=") :] + previous_data = unquote(request.GET.urlencode()) + previous_data = ( + previous_data[3:] if previous_data.startswith("pd=") else previous_data + ) biometric_form = BiometricDeviceForm() if request.method == "POST": biometric_form = BiometricDeviceForm(request.POST) @@ -554,7 +571,7 @@ def biometric_device_add(request): messages.success(request, _("Biometric device added successfully.")) biometric_form = BiometricDeviceForm() context = {"biometric_form": biometric_form, "pd": previous_data} - return render(request, "biometric/add_biometric_device.html", context) + return render(request, "biometric/biometric_device_form.html", context) @login_required @@ -576,7 +593,7 @@ def biometric_device_edit(request, device_id): device = BiometricDevices.find(device_id) if not device: messages.error(request, _("Biometric device not found.")) - return render(request, "biometric/edit_biometric_device.html") + return render(request, "biometric/biometric_device_form.html") biometric_form = BiometricDeviceForm(instance=device) if request.method == "POST": biometric_form = BiometricDeviceForm(request.POST, instance=device) @@ -587,7 +604,7 @@ def biometric_device_edit(request, device_id): "biometric_form": biometric_form, "device_id": device_id, } - return render(request, "biometric/edit_biometric_device.html", context) + return render(request, "biometric/biometric_device_form.html", context) @login_required @@ -602,12 +619,12 @@ def biometric_device_archive(request, device_id): device_obj = BiometricDevices.find(device_id) if not device_obj: messages.error(request, _("Biometric device not found.")) - return redirect(f"/biometric/search-devices?{previous_data}") + return redirect(f"/biometric/view-biometric-devices/?{previous_data}") device_obj.is_active = not device_obj.is_active device_obj.save() message = _("archived") if not device_obj.is_active else _("un-archived") messages.success(request, _("Device is %(message)s") % {"message": message}) - return redirect(f"/biometric/search-devices?{previous_data}") + return redirect(f"/biometric/view-biometric-devices/?{previous_data}") @login_required @@ -623,7 +640,7 @@ def biometric_device_delete(request, device_id): - device_id (uuid): The ID of the biometric device to be deleted. Returns: - - HttpResponseRedirect: Redirects to the 'search-devices' page after deleting the + - HttpResponseRedirect: Redirects to the 'view-biometric-devices' page after deleting the biometric device. """ @@ -631,45 +648,10 @@ def biometric_device_delete(request, device_id): device_obj = BiometricDevices.find(device_id) if not device_obj: messages.error(request, _("Biometric device not found.")) - return redirect(f"/biometric/search-devices?{previous_data}") + return redirect(f"/biometric/view-biometric-devices/?{previous_data}") device_obj.delete() messages.success(request, _("Biometric device deleted successfully.")) - return redirect(f"/biometric/search-devices?{previous_data}") - - -@login_required -@install_required -@hx_request_required -@permission_required("biometric.view_biometricdevices") -def search_devices(request): - """ - This method is used to search biometric device model and return matching objects - """ - previous_data = request.GET.urlencode() - search = request.GET.get("search") - is_active = request.GET.get("is_active") - if search is None: - search = "" - devices = BiometricDeviceFilter(request.GET).qs.order_by("-created_at") - if not is_active or is_active == "unknown": - devices = devices.filter(is_active=True) - data_dict = [] - data_dict = parse_qs(previous_data) - get_key_instances(BiometricDevices, data_dict) - template = "biometric/card_biometric_devices.html" - if request.GET.get("view") == "list": - template = "biometric/list_biometric_devices.html" - - devices = paginator_qry(devices, request.GET.get("page")) - return render( - request, - template, - { - "devices": devices, - "pd": previous_data, - "filter_dict": data_dict, - }, - ) + return redirect(f"/biometric/view-biometric-devices/?{previous_data}") def render_connection_response(title, text, icon): @@ -911,7 +893,7 @@ def biometric_device_bulk_fetch_logs(request): ) return HttpResponse(script) - attendance_count, error_message = zk_biometric_attendance_bulk_logs(zk_devices) + attendance_count, error_message = zk_biometric_attendance_logs(zk_devices) if isinstance(attendance_count, int): script = render_connection_response( _("Logs Fetched Successfully"), @@ -2163,91 +2145,28 @@ def biometric_device_live(request): return HttpResponse(script) -def zk_biometric_attendance_logs(device): +def zk_biometric_attendance_logs(device_or_devices): """ - Retrieve attendance records from a ZK biometric device and update the clock-in/clock-out status. + Retrieve and process attendance logs from one or more ZKTeco biometric devices. - :param device_id: The ID of the ZK biometric device. + Handles scenarios where the same user_id may exist across multiple devices for the same employee. + + :param device_or_devices: A single BiometricDevice instance or a queryset/list of them. + :return: Tuple (number_of_attendance_processed, error_message or None) """ - port_no = device.port - machine_ip = device.machine_ip - conn = None - zk_device = ZK( - machine_ip, - port=port_no, - timeout=5, - password=int(device.zk_password), - force_udp=False, - ommit_ping=False, - ) - try: - conn = zk_device.connect() - conn.enable_device() - attendances = conn.get_attendance() - last_attendance_datetime = attendances[-1].timestamp - if device.last_fetch_date and device.last_fetch_time: - filtered_attendances = [ - attendance - for attendance in attendances - if (attendance.timestamp.date() > device.last_fetch_date) - or ( - attendance.timestamp.date() == device.last_fetch_date - and attendance.timestamp.time() > device.last_fetch_time - ) - ] - else: - filtered_attendances = attendances - device.last_fetch_date = last_attendance_datetime.date() - device.last_fetch_time = last_attendance_datetime.time() - device.save() - bio_id_map = { - bio.user_id: bio - for bio in BiometricEmployees.objects.filter(device_id=device) - } - for attendance in filtered_attendances: - user_id = attendance.user_id - punch_code = attendance.punch - date_time = django_timezone.make_aware(attendance.timestamp) - date = date_time.date() - time = date_time.time() - bio_id = bio_id_map.get(user_id) - if bio_id: - request_data = Request( - user=bio_id.employee_id.employee_user_id, - date=date, - time=time, - datetime=date_time, - ) + if hasattr(device_or_devices, "__iter__") and not isinstance( + device_or_devices, dict + ): + devices = list(device_or_devices) + else: + devices = [device_or_devices] - if punch_code in {0, 3, 4}: - try: - clock_in(request_data) - except Exception as error: - logger.error("Got an error : ", error) - elif punch_code in {1, 2, 5}: - try: - clock_out(request_data) - except Exception as error: - logger.error("Got an error : ", error) - else: - pass - return len(filtered_attendances), None - except zk_exception.ZKErrorResponse as e: - return "error", str(e) - except Exception as e: - logger.error("ZKTeco connection error", exc_info=True) - return "error", str(e) - finally: - if conn: - conn.disconnect() - - -def zk_biometric_attendance_bulk_logs(devices): errors = [] combined_attendances = [] + patch_direction = {"in": 0, "out": 1} bio_id_map = { - bio.user_id: bio + (bio.device_id_id, bio.user_id): bio for bio in BiometricEmployees.objects.filter(device_id__in=devices) } @@ -2286,13 +2205,17 @@ def zk_biometric_attendance_bulk_logs(devices): else: filtered = attendances - # Track for final device update + # Update last fetch markers device.last_fetch_date = last_attendance_datetime.date() device.last_fetch_time = last_attendance_datetime.time() device.save() - for attendance in filtered: - attendance.device = device # Attach device info for later processing + attendance.device = device # Attach device info + attendance.punch = ( + patch_direction[device.device_direction] + if device.device_direction in patch_direction + else attendance.punch + ) # Update punch code based on device direction combined_attendances.append(attendance) except zk_exception.ZKErrorResponse as e: @@ -2304,7 +2227,7 @@ def zk_biometric_attendance_bulk_logs(devices): if conn: conn.disconnect() - # Process all combined attendances (after sorting based on timestamp) + # Sort all filtered attendances by time combined_attendances.sort(key=lambda a: a.timestamp) for attendance in combined_attendances: @@ -2313,7 +2236,8 @@ def zk_biometric_attendance_bulk_logs(devices): date_time = django_timezone.make_aware(attendance.timestamp) date = date_time.date() time = date_time.time() - bio_id = bio_id_map.get(user_id) + device_id = attendance.device.id + bio_id = bio_id_map.get((device_id, user_id)) if bio_id: request_data = Request( user=bio_id.employee_id.employee_user_id, @@ -2326,9 +2250,10 @@ def zk_biometric_attendance_bulk_logs(devices): clock_in(request_data) elif punch_code in {1, 2, 5}: clock_out(request_data) - except Exception as error: + except Exception: logger.error( - f"[Device: {attendance.device.name}] Punch error", exc_info=True + f"[Device: {attendance.device.name}] Punch processing error", + exc_info=True, ) return len(combined_attendances), "; ".join(errors) if errors else None diff --git a/static/index/index.js b/static/index/index.js index 36a3b93a3..c3b2414fc 100644 --- a/static/index/index.js +++ b/static/index/index.js @@ -431,6 +431,28 @@ function handleHtmxTarget(event, path, verb) { return hxTarget; } +function hxConfirm(element, messageText) { + Swal.fire({ + html: messageText, + icon: "question", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + cancelButtonText: "Cancel", + reverseButtons: true, + }).then((result) => { + if (result.isConfirmed) { + htmx.trigger(element, 'confirmed'); + } + else { + element.checked = false + return false + } + + }); +} + function handleDownloadAndRefresh(event, url) { // Use in import_popup.html file event.preventDefault();