[ADD] BIOMETRIC : Add integration for e-Time Office biometric device
This commit is contained in:
91
biometric/etimeoffice.py
Normal file
91
biometric/etimeoffice.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ETimeOfficeAPI:
|
||||
def __init__(self, username, password, base_url="https://api.etimeoffice.com/api/"):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.base_url = base_url.rstrip("/") + "/"
|
||||
|
||||
def _is_valid_date(self, date_str, with_time=True):
|
||||
try:
|
||||
if with_time:
|
||||
datetime.strptime(date_str, "%d/%m/%Y_%H:%M")
|
||||
else:
|
||||
datetime.strptime(date_str, "%d/%m/%Y")
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _convert_punch_dates(self, response_data):
|
||||
if not response_data.get("Error", True):
|
||||
if "PunchData" in response_data:
|
||||
for punch in response_data["PunchData"]:
|
||||
try:
|
||||
punch["PunchDate"] = datetime.strptime(
|
||||
punch["PunchDate"], "%d/%m/%Y %H:%M:%S"
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
if "InOutPunchData" in response_data:
|
||||
for punch in response_data["InOutPunchData"]:
|
||||
try:
|
||||
punch["DateString"] = datetime.strptime(
|
||||
punch["DateString"], "%d/%m/%Y"
|
||||
).date()
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
punch["INTime"] = datetime.strptime(
|
||||
punch["INTime"], "%H:%M"
|
||||
).time()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
punch["OUTTime"] = datetime.strptime(
|
||||
punch["OUTTime"], "%H:%M"
|
||||
).time()
|
||||
except:
|
||||
pass
|
||||
return response_data
|
||||
|
||||
def _fetch_data(self, endpoint, emp_code, from_date, to_date, with_time=True):
|
||||
if not (
|
||||
self._is_valid_date(from_date, with_time)
|
||||
and self._is_valid_date(to_date, with_time)
|
||||
):
|
||||
return {
|
||||
"Error": True,
|
||||
"Msg": "Error: Invalid date format. Expected format: "
|
||||
+ ("DD/MM/YYYY_HH:MM" if with_time else "DD/MM/YYYY"),
|
||||
}
|
||||
|
||||
url = f"{self.base_url}{endpoint}?Empcode={emp_code}&FromDate={from_date}&ToDate={to_date}"
|
||||
response = requests.get(url, auth=HTTPBasicAuth(self.username, self.password))
|
||||
return self._convert_punch_dates(response.json())
|
||||
|
||||
def download_punch_data(self, from_date, to_date, emp_code="ALL"):
|
||||
return self._fetch_data(
|
||||
"DownloadPunchData", emp_code, from_date, to_date, with_time=True
|
||||
)
|
||||
|
||||
def download_punch_data_mcid(self, from_date, to_date, emp_code="ALL"):
|
||||
return self._fetch_data(
|
||||
"DownloadPunchDataMCID", emp_code, from_date, to_date, with_time=True
|
||||
)
|
||||
|
||||
def download_in_out_punch_data(self, from_date, to_date, emp_code="ALL"):
|
||||
return self._fetch_data(
|
||||
"DownloadInOutPunchData", emp_code, from_date, to_date, with_time=False
|
||||
)
|
||||
|
||||
|
||||
# api = ETimeOfficeAPI(username={corporateid}:{usename}:{password}:true",password="")
|
||||
# response = api.download_punch_data(from_date="25/03/2025_00:00",to_date="25/03/2025_12:22")
|
||||
@@ -6,6 +6,7 @@ employee biometric data, COSEC users, and related configurations.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from attendance.forms import ModelForm
|
||||
@@ -289,7 +290,7 @@ class DahuaUserForm(Form):
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class DahuaMapUsers(ModelForm):
|
||||
class MapBioUsers(ModelForm):
|
||||
class Meta:
|
||||
model = BiometricEmployees
|
||||
fields = ["employee_id", "user_id"]
|
||||
@@ -307,6 +308,24 @@ class DahuaMapUsers(ModelForm):
|
||||
device_id=self.device_id
|
||||
).values_list("employee_id", flat=True)
|
||||
self.fields["employee_id"].queryset = Employee.objects.exclude(
|
||||
id__in=already_mapped_employees
|
||||
Q(id__in=already_mapped_employees) | Q(is_active=False)
|
||||
)
|
||||
self.fields["user_id"].required = True
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
user_id = cleaned_data.get("user_id")
|
||||
user_id_label = self.fields["user_id"].label or "User ID"
|
||||
if self.device_id and user_id:
|
||||
if BiometricEmployees.objects.filter(
|
||||
user_id=user_id, device_id=self.device_id
|
||||
).exists():
|
||||
raise forms.ValidationError(
|
||||
{
|
||||
"user_id": _(
|
||||
"This biometric {} is already mapped with an employee"
|
||||
).format(user_id_label)
|
||||
}
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
@@ -50,6 +50,7 @@ class BiometricDevices(HorillaModel):
|
||||
("anviz", _("Anviz Biometric")),
|
||||
("cosec", _("Matrix COSEC Biometric")),
|
||||
("dahua", _("Dahua Biometric")),
|
||||
("etimeoffice", _("e-Time Office")),
|
||||
]
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
@@ -33,7 +33,14 @@ function selectAllDahuaUsers(element) {
|
||||
$(".all-bio-employee-row").prop("checked", false);
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllETimeOfficeUsers(element) {
|
||||
var is_checked = $("#allBioEmployee").is(":checked");
|
||||
if (is_checked) {
|
||||
$(".all-bio-employee-row").prop("checked", true);
|
||||
} else {
|
||||
$(".all-bio-employee-row").prop("checked", false);
|
||||
}
|
||||
}
|
||||
|
||||
$(".all-bio-employee-row").change(function (e) {
|
||||
var is_checked = $(".all-bio-employee").is(":checked");
|
||||
@@ -187,6 +194,43 @@ function deleteDahuaUsers(e) {
|
||||
});
|
||||
}
|
||||
|
||||
function deleteETimeOfficeUsers(e) {
|
||||
var languageCode = null;
|
||||
getCurrentLanguageCode(function (code) {
|
||||
languageCode = code;
|
||||
var confirmMessage = deleteUsersMessages[languageCode];
|
||||
var textMessage = nousersdeleteMessages[languageCode];
|
||||
var checkedRows = $(".all-bio-employee-row").filter(":checked");
|
||||
if (checkedRows.length === 0) {
|
||||
Swal.fire({
|
||||
text: textMessage,
|
||||
icon: "warning",
|
||||
confirmButtonText: "Close",
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
text: confirmMessage,
|
||||
icon: "error",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#008000",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Confirm",
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
ids = [];
|
||||
checkedRows.each(function () {
|
||||
ids.push($(this).attr("id"));
|
||||
});
|
||||
var hxValue = JSON.stringify(ids);
|
||||
var bioDeviceID = JSON.stringify($("#allBioEmployee").data("device"))
|
||||
$("#deleteETimeOfficeUsers").attr("hx-vals", `{"ids":${hxValue},"device_id":${bioDeviceID}}`);
|
||||
$("#deleteETimeOfficeUsers").click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// ******************************************************************
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
data-toggle="oh-modal-toggle" data-target="#BiometricDeviceModal"
|
||||
hx-target="#BiometricDeviceFormTarget">{% trans "Schedule" %}</a>
|
||||
{% endif %}
|
||||
{% if device.machine_type == "zk" or device.machine_type == "cosec" or device.machine_type == "dahua" %}
|
||||
{% if device.machine_type == "zk" or device.machine_type == "cosec" or device.machine_type == "dahua" or device.machine_type == "etimeoffice" %}
|
||||
<a href="{% url 'biometric-device-employees' device.id %}"
|
||||
class="oh-checkpoint-badge text-secondary bio-user-list">{% trans "Employee" %}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
// Show relevant elements based on the selected machine type
|
||||
switch (machineType) {
|
||||
case "anviz":
|
||||
$("input[name='api_url']").val("");
|
||||
$("#apiUrlInput, #apiKeyInput, #apiSecretInput, #apiRequestIDInput").show();
|
||||
break;
|
||||
case "zk":
|
||||
@@ -59,6 +60,13 @@
|
||||
case "cosec":
|
||||
case "dahua":
|
||||
$("#machinIpInput, #machinUserName, #machinPassword, #machinPortInput").show();
|
||||
break;
|
||||
case "etimeoffice":
|
||||
$("#apiUrlInput, #machinUserName, #machinPassword").show();
|
||||
if (!$("input[name='api_url']").val()) {
|
||||
$("input[name='api_url']").val("https://api.etimeoffice.com/api/");
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
// No elements need to be shown for unknown machine types
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{% load i18n %} {% load static %}
|
||||
<div class="oh-sticky-table">
|
||||
<div class="oh-sticky-table__table oh-table--sortable">
|
||||
<div class="oh-sticky-table__thead">
|
||||
<div class="oh-sticky-table__tr">
|
||||
<div class="oh-sticky-table__th" style="width: 10px">
|
||||
<div class="centered-div">
|
||||
<input type="checkbox" class="oh-input oh-input__checkbox all-bio-employee"
|
||||
data-device="{{device_id}}" id="allBioEmployee" hx-on:click="selectAllETimeOfficeUsers(event)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-sticky-table__th">{% trans "Employee" %}</div>
|
||||
<div class="oh-sticky-table__th">{% trans "Badge ID" %}</div>
|
||||
<div class="oh-sticky-table__th">{% trans "Emp Code" %}</div>
|
||||
<div class="oh-sticky-table__th">{% trans "Work Email" %}</div>
|
||||
<div class="oh-sticky-table__th">{% trans "Phone" %}</div>
|
||||
<div class="oh-sticky-table__th">{% trans "Job Position" %}</div>
|
||||
<div class="oh-sticky-table__th oh-sticky-table__right" style="width: 210px;">
|
||||
{% trans "Actions" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for employee in employees %}
|
||||
<div class="oh-sticky-table__tbody ui-sortable fade-me-out" id="eTimeOfficeUser{{employee.id}}">
|
||||
<div class="oh-sticky-table__tr ui-sortable-handle">
|
||||
<div class="oh-sticky-table__sd">
|
||||
<div class="centered-div">
|
||||
<input type="checkbox" id="{{employee.user_id}}"
|
||||
class="form-check-input all-bio-employee-row" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-sticky-table__td">
|
||||
{{employee.employee_id.get_full_name}}
|
||||
</div>
|
||||
<div class="oh-sticky-table__td">
|
||||
{{employee.employee_id.badge_id}}
|
||||
</div>
|
||||
<div class="oh-sticky-table__td">
|
||||
{{employee.user_id}}
|
||||
</div>
|
||||
<div class="oh-sticky-table__td">
|
||||
{{employee.employee_id.employee_work_info.email}}
|
||||
</div>
|
||||
<div class="oh-sticky-table__td">{{employee.employee_id.phone}}</div>
|
||||
<div class="oh-sticky-table__td">
|
||||
{{employee.employee_id.get_job_position}}
|
||||
</div>
|
||||
<div class="oh-sticky-table__td oh-sticky-table__right">
|
||||
<div class="oh-btn-group">
|
||||
<a class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
|
||||
hx-confirm="{% trans 'Are you sure you want to delete this user?' %}"
|
||||
hx-post="{% url 'delete-etimeoffice-user' employee.id %}" hx-target="#eTimeOfficeUser{{employee.id}}"
|
||||
hx-swap="outerHTML swap:.5s">
|
||||
<ion-icon name="trash-outline" role="img" class="md hydrated" aria-label="trash outline">
|
||||
</ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
{% load i18n %}
|
||||
{% if messages %}
|
||||
<script>reloadMessage();</script>
|
||||
<span hx-get="{% url 'biometric-device-employees' device_id %}" hx-target="#eTimeOfficeUsersList" hx-select="#eTimeOfficeUsersList" hx-trigger="load delay:500ms" hx-swap="outerHTML"
|
||||
></span>
|
||||
{% endif %}
|
||||
<div class="oh-modal__dialog-header">
|
||||
<span class="oh-modal__dialog-title">
|
||||
{% trans "Map Dahua User" %}
|
||||
</span>
|
||||
<button class="oh-modal__close" aria-label="Close">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="oh-modal__dialog-body">
|
||||
<form hx-post="{%url 'map-biometric-users' device_id %}" hx-target="#objectCreateModalTarget" class="oh-profile-section pt-4">
|
||||
{{form.as_p}}
|
||||
<div class="oh-modal__dialog-footer p-0 pt-3">
|
||||
<button type="submit" class="oh-btn oh-btn--secondary oh-btn--shadow">
|
||||
{% trans "Map" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
{% load i18n %}{% load static %}
|
||||
<section class="oh-main__topbar" x-data="{searchShow: false}">
|
||||
<div class="oh-main__titlebar oh-main__titlebar--left">
|
||||
<div class="oh-main__titlebar-title fw-bold mb-0 text-dark">
|
||||
{% trans "Employees" %}
|
||||
</div>
|
||||
<a class="oh-main__titlebar-search-toggle" role="button" aria-label="Toggle Search"
|
||||
@click="searchShow = !searchShow">
|
||||
<ion-icon name="search-outline" class="oh-main__titlebar-serach-icon"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
<div class="oh-input-group oh-input__search-group" :class="searchShow ? 'oh-input__search-group--show' : ''">
|
||||
<ion-icon name="search-outline" class="oh-input-group__icon oh-input-group__icon--left"></ion-icon>
|
||||
<input type="text" hx-get="{% url 'search-employee-in-device' %}?device={{device_id}}"
|
||||
placeholder="{% trans 'Search' %}" name="search" hx-trigger="keyup changed delay:.2s"
|
||||
hx-target="#eTimeOfficeUsersList" class="oh-input oh-input__icon" hx-vals='{"view":"{{request.GET.view}}"}'
|
||||
aria-label="Search Input" />
|
||||
</div>
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-btn-group ml-2">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button class="oh-btn oh-btn--dropdown oh-btn oh-btn--shadow" @click="open = !open"
|
||||
@click.outside="open = false">
|
||||
{% trans "Actions" %}
|
||||
</button>
|
||||
<div class="oh-dropdown__menu oh-dropdown__menu--right" x-show="open" style="display: none">
|
||||
<ul class="oh-dropdown__items">
|
||||
<li class="oh-dropdown__item">
|
||||
<a href="#" class="oh-dropdown__link" data-toggle="oh-modal-toggle"
|
||||
data-target="#objectCreateModal" hx-get="{%url 'map-biometric-users' device_id %}"
|
||||
hx-target="#objectCreateModalTarget">
|
||||
{% trans "Map Employee" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if perms.delete_biometricdevices %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a href="#" class="oh-dropdown__link oh-dropdown__link--danger"
|
||||
onclick="deleteETimeOfficeUsers(this)" data-action="delete">{% trans "Delete" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends 'index.html' %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
<div class="oh-wrapper">
|
||||
{% include 'biometric_users/etimeoffice/nav_etimeoffice_employees.html' %}
|
||||
<div id="eTimeOfficeUsersList">
|
||||
{% include 'biometric_users/etimeoffice/list_etimeoffice_employees.html' %}
|
||||
</div>
|
||||
<div class="oh-modal" id="BiometricDeviceModal" role="dialog" aria-labelledby="BiometricDeviceModal"
|
||||
aria-hidden="true">
|
||||
<div class="oh-modal__dialog " style="max-width: 550px;" id="BiometricDeviceFormTarget"></div>
|
||||
</div>
|
||||
<div class="oh-modal" id="BiometricDeviceTestModal" role="dialog" aria-labelledby="BiometricDeviceTestModal"
|
||||
aria-hidden="true">
|
||||
<div class="oh-modal__dialog" style="max-width: 550px" id="BiometricDeviceTestFormTarget">
|
||||
{% include "animation.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<span hx-delete="{% url 'delete-etimeoffice-user' %}" hx-vals="" id="deleteETimeOfficeUsers"></span>
|
||||
</div>
|
||||
<script src="{% static 'actions.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -113,6 +113,16 @@ urlpatterns = [
|
||||
views.delete_dahua_user,
|
||||
name="delete-dahua-user",
|
||||
),
|
||||
path(
|
||||
"delete-etimeoffice-user",
|
||||
views.delete_etimeoffice_user,
|
||||
name="delete-etimeoffice-user",
|
||||
),
|
||||
path(
|
||||
"delete-etimeoffice-user/<uuid:obj_id>",
|
||||
views.delete_etimeoffice_user,
|
||||
name="delete-etimeoffice-user",
|
||||
),
|
||||
path(
|
||||
"enable-cosec-face-recognition/<str:user_id>/<uuid:device_id>/",
|
||||
views.enable_cosec_face_recognition,
|
||||
|
||||
@@ -30,6 +30,7 @@ from attendance.models import AttendanceActivity
|
||||
from attendance.views.clock_in_out import clock_in, clock_out
|
||||
from base.methods import get_key_instances, get_pagination
|
||||
from biometric.anviz import CrossChexCloudAPI
|
||||
from biometric.etimeoffice import ETimeOfficeAPI
|
||||
from employee.models import Employee, EmployeeWorkInformation
|
||||
from horilla.decorators import (
|
||||
hx_request_required,
|
||||
@@ -49,9 +50,9 @@ from .forms import (
|
||||
BiometricDeviceSchedulerForm,
|
||||
CosecUserAddForm,
|
||||
COSECUserForm,
|
||||
DahuaMapUsers,
|
||||
DahuaUserForm,
|
||||
EmployeeBiometricAddForm,
|
||||
MapBioUsers,
|
||||
)
|
||||
from .models import BiometricDevices, BiometricEmployees, COSECAttendanceArguments
|
||||
|
||||
@@ -463,7 +464,7 @@ def biometric_device_schedule(request, device_id):
|
||||
)
|
||||
scheduler.start()
|
||||
return HttpResponse("<script>window.location.reload()</script>")
|
||||
else:
|
||||
elif device.machine_type == "cosec":
|
||||
duration = request.POST.get("scheduler_duration")
|
||||
device.is_scheduler = True
|
||||
device.is_live = False
|
||||
@@ -481,6 +482,22 @@ def biometric_device_schedule(request, device_id):
|
||||
)
|
||||
scheduler.start()
|
||||
return HttpResponse("<script>window.location.reload()</script>")
|
||||
elif device.machine_type == "etimeoffice":
|
||||
duration = request.POST.get("scheduler_duration")
|
||||
device.is_scheduler = True
|
||||
device.is_live = False
|
||||
device.scheduler_duration = duration
|
||||
device.save()
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(
|
||||
lambda: etimeoffice_biometric_attendance_scheduler(device.id),
|
||||
"interval",
|
||||
seconds=str_time_seconds(device.scheduler_duration),
|
||||
)
|
||||
scheduler.start()
|
||||
return HttpResponse("<script>window.location.reload()</script>")
|
||||
else:
|
||||
return HttpResponse("<script>window.location.reload()</script>")
|
||||
|
||||
context["scheduler_form"] = scheduler_form
|
||||
response = render(request, "biometric/scheduler_device_form.html", context)
|
||||
@@ -786,6 +803,45 @@ def test_dahua_connection(device):
|
||||
)
|
||||
|
||||
|
||||
def test_etimeoffice_connection(device):
|
||||
"""Test connection for e-TimeOffice device."""
|
||||
now = datetime.now()
|
||||
etimeoffice = ETimeOfficeAPI(
|
||||
username=device.bio_username,
|
||||
password=device.bio_password,
|
||||
)
|
||||
|
||||
from_date = f"{now.day:02d}/{now.month:02d}/{now.year}_00:00"
|
||||
to_date = (
|
||||
f"{now.day:02d}/{now.month:02d}/{now.year}_{now.hour:02d}:{now.minute:02d}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = etimeoffice.download_punch_data(from_date=from_date, to_date=to_date)
|
||||
if response.get("Msg") == "Success":
|
||||
return render_connection_response(
|
||||
_("Connection Successful"),
|
||||
_("e-Time Office test connection successful."),
|
||||
"success",
|
||||
)
|
||||
|
||||
error_msg = response.get("Message")
|
||||
return render_connection_response(
|
||||
_("Connection unsuccessful"),
|
||||
_("Double-check the provided API Url, Username, and Password: {}").format(
|
||||
error_msg
|
||||
),
|
||||
"warning",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return render_connection_response(
|
||||
_("Connection error"),
|
||||
_(f"API request failed with exception: {str(e)}"),
|
||||
"danger",
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@install_required
|
||||
@hx_request_required
|
||||
@@ -810,6 +866,8 @@ def biometric_device_test(request, device_id):
|
||||
script = test_cosec_connection(device)
|
||||
elif device.machine_type == "dahua":
|
||||
script = test_dahua_connection(device)
|
||||
elif device.machine_type == "etimeoffice":
|
||||
script = test_etimeoffice_connection(device)
|
||||
else:
|
||||
script = render_connection_response(
|
||||
"Connection unsuccessful",
|
||||
@@ -890,7 +948,6 @@ def biometric_device_fetch_logs(request, device_id):
|
||||
"warning",
|
||||
)
|
||||
elif device.machine_type == "dahua":
|
||||
script = test_dahua_connection(device)
|
||||
attendance_count = dahua_biometric_attendance_logs(device)
|
||||
if isinstance(attendance_count, int):
|
||||
script = render_connection_response(
|
||||
@@ -906,6 +963,22 @@ def biometric_device_fetch_logs(request, device_id):
|
||||
_("Double-check the provided Machine IP, Username, and Password."),
|
||||
"warning",
|
||||
)
|
||||
elif device.machine_type == "etimeoffice":
|
||||
attendance_count = etimeoffice_biometric_attendance_logs(device)
|
||||
if isinstance(attendance_count, int):
|
||||
script = render_connection_response(
|
||||
_("Logs Fetched Successfully"),
|
||||
_(
|
||||
f"Biometric attendance logs fetched successfully. Total records: {attendance_count}"
|
||||
),
|
||||
"success",
|
||||
)
|
||||
else:
|
||||
script = render_connection_response(
|
||||
_("Connection unsuccessful"),
|
||||
_("Double-check the provided API Url, Username, and Password"),
|
||||
"warning",
|
||||
)
|
||||
else:
|
||||
script = render_connection_response(
|
||||
"Connection unsuccessful",
|
||||
@@ -1154,6 +1227,17 @@ def biometric_device_employees(request, device_id, **kwargs):
|
||||
return render(
|
||||
request, "biometric_users/dahua/view_dahua_employees.html", context
|
||||
)
|
||||
if device.machine_type == "etimeoffice":
|
||||
employees = BiometricEmployees.objects.filter(device_id=device_id)
|
||||
context = {
|
||||
"device_id": device.id,
|
||||
"employees": employees,
|
||||
}
|
||||
return render(
|
||||
request,
|
||||
"biometric_users/etimeoffice/view_etimeoffice_employees.html",
|
||||
context,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("An error occurred: ", error)
|
||||
messages.info(
|
||||
@@ -1207,13 +1291,17 @@ def search_employee_device(request):
|
||||
"device_id": device_id,
|
||||
"pd": previous_data,
|
||||
}
|
||||
elif device.machine_type == "dahua":
|
||||
elif device.machine_type == "dahua" or device.machine_type == "etimeoffice":
|
||||
search_employees = BiometricEmployees.objects.filter(device_id=device)
|
||||
if search:
|
||||
search_employees = BiometricEmployees.objects.filter(
|
||||
employee_id__employee_first_name__icontains=search, device_id=device
|
||||
)
|
||||
template = "biometric_users/dahua/list_dahua_employees.html"
|
||||
template = (
|
||||
"biometric_users/dahua/list_dahua_employees.html"
|
||||
if device.machine_type == "dahua"
|
||||
else "biometric_users/etimeoffice/list_etimeoffice_employees.html"
|
||||
)
|
||||
context = {
|
||||
"device_id": device.id,
|
||||
"employees": search_employees,
|
||||
@@ -1695,24 +1783,36 @@ def add_biometric_user(request, device_id):
|
||||
@install_required
|
||||
@hx_request_required
|
||||
def map_biometric_users(request, device_id):
|
||||
form = DahuaMapUsers()
|
||||
if request.method == "POST":
|
||||
form = DahuaMapUsers(request.POST)
|
||||
if form.is_valid():
|
||||
user_id = request.POST.get("user_id")
|
||||
employee_id = request.POST.get("employee_id")
|
||||
employee = Employee.objects.filter(id=employee_id).first()
|
||||
device = BiometricDevices.find(device_id)
|
||||
if device and employee_id:
|
||||
created = BiometricEmployees.objects.create(
|
||||
user_id=user_id, employee_id=employee, device_id=device
|
||||
)
|
||||
messages.success(
|
||||
request,
|
||||
_("Selected employee successfully mapped to the biometric user"),
|
||||
)
|
||||
context = {"form": form, "device_id": device_id}
|
||||
return render(request, "biometric_users/dahua/map_dahua_users.html", context)
|
||||
device = BiometricDevices.find(device_id)
|
||||
form = MapBioUsers(request.POST or None)
|
||||
template = "biometric_users/dahua/map_dahua_users.html"
|
||||
|
||||
if device.machine_type == "etimeoffice":
|
||||
template = "biometric_users/etimeoffice/map_etimeoffice_users.html"
|
||||
form.fields["user_id"].label = _("Emp Code")
|
||||
|
||||
if request.method == "POST" and form.is_valid():
|
||||
user_id = form.cleaned_data["user_id"]
|
||||
employee = form.cleaned_data["employee_id"]
|
||||
if device and employee:
|
||||
BiometricEmployees.objects.create(
|
||||
user_id=user_id, employee_id=employee, device_id=device
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Selected employee successfully mapped to the biometric user"),
|
||||
)
|
||||
form = MapBioUsers()
|
||||
|
||||
if device.machine_type == "etimeoffice":
|
||||
form.fields["user_id"].label = _("Emp Code")
|
||||
|
||||
return render(
|
||||
request,
|
||||
template,
|
||||
{"form": form, "device_id": device_id},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1830,6 +1930,47 @@ def delete_dahua_user(request, obj_id=None):
|
||||
return HttpResponse(script)
|
||||
|
||||
|
||||
@login_required
|
||||
@install_required
|
||||
@hx_request_required
|
||||
@permission_required("biometric.delete_biometricemployees")
|
||||
def delete_etimeoffice_user(request, obj_id=None):
|
||||
script = "<script>window.location.href = '/';</script>"
|
||||
if request.method == "POST":
|
||||
user = BiometricEmployees.objects.get(id=obj_id)
|
||||
device_id = user.device_id.id
|
||||
user.delete()
|
||||
messages.success(
|
||||
request, _("{} successfully deleted!").format(user.employee_id)
|
||||
)
|
||||
script = "<script>reloadMessage();</script>"
|
||||
if request.method == "DELETE":
|
||||
user_ids = request.GET.getlist("ids")
|
||||
device_id = request.GET.get("device_id")
|
||||
if device_id:
|
||||
script = f"""
|
||||
<span hx-get="/biometric/biometric-device-employees/{device_id}/"
|
||||
hx-target="#eTimeOfficeUsersList" hx-select="#eTimeOfficeUsersList" hx-trigger="load delay:200ms"
|
||||
hx-swap="outerHTML" hx-on-htmx-before-request="reloadMessage();">
|
||||
</span>
|
||||
"""
|
||||
if user_ids:
|
||||
users = BiometricEmployees.objects.filter(user_id__in=user_ids)
|
||||
if users:
|
||||
count = users.count()
|
||||
users.delete()
|
||||
messages.success(
|
||||
request, _("{} users successfully deleted!").format(count)
|
||||
),
|
||||
else:
|
||||
messages.warning(
|
||||
request,
|
||||
_("No rows are selected for deleting users from device."),
|
||||
)
|
||||
|
||||
return HttpResponse(script)
|
||||
|
||||
|
||||
@login_required
|
||||
@install_required
|
||||
@hx_request_required
|
||||
@@ -1977,8 +2118,11 @@ def zk_biometric_attendance_logs(device):
|
||||
filtered_attendances = [
|
||||
attendance
|
||||
for attendance in attendances
|
||||
if attendance.timestamp.date() >= device.last_fetch_date
|
||||
and attendance.timestamp.time() > device.last_fetch_time
|
||||
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
|
||||
@@ -2250,6 +2394,79 @@ def dahua_biometric_attendance_scheduler(device_id):
|
||||
dahua_biometric_attendance_logs(device)
|
||||
|
||||
|
||||
def etimeoffice_biometric_attendance_logs(device):
|
||||
now = datetime.now()
|
||||
etimeoffice = ETimeOfficeAPI(
|
||||
username=device.bio_username,
|
||||
password=device.bio_password,
|
||||
)
|
||||
|
||||
from_date = (
|
||||
f"{(datetime.combine(device.last_fetch_date, device.last_fetch_time) + timedelta(minutes=1)):%d/%m/%Y_%H:%M}"
|
||||
if device.last_fetch_date and device.last_fetch_time
|
||||
else f"{now:%d/%m/%Y}_00:00"
|
||||
)
|
||||
to_date = f"{now:%d/%m/%Y_%H:%M}"
|
||||
|
||||
logs = etimeoffice.download_punch_data(from_date=from_date, to_date=to_date)
|
||||
if logs.get("Msg") != "Success":
|
||||
return "error"
|
||||
|
||||
punch_data = logs.get("PunchData", [])
|
||||
if not punch_data:
|
||||
return 0
|
||||
|
||||
user_tz = pytz.timezone(TIME_ZONE)
|
||||
|
||||
employee_map = {
|
||||
emp.user_id: emp for emp in BiometricEmployees.objects.filter(device_id=device)
|
||||
}
|
||||
|
||||
for log in reversed(punch_data):
|
||||
user_id = log.get("Empcode")
|
||||
if not user_id or user_id not in employee_map:
|
||||
continue
|
||||
|
||||
employee = employee_map[user_id]
|
||||
attendance_datetime = log["PunchDate"].astimezone(user_tz)
|
||||
|
||||
request_data = Request(
|
||||
user=employee.employee_id.employee_user_id,
|
||||
date=attendance_datetime.date(),
|
||||
time=attendance_datetime.time(),
|
||||
datetime=attendance_datetime,
|
||||
)
|
||||
|
||||
last_none_activity = (
|
||||
AttendanceActivity.objects.filter(
|
||||
employee_id=employee.employee_id,
|
||||
clock_out=None,
|
||||
)
|
||||
.order_by("in_datetime")
|
||||
.last()
|
||||
)
|
||||
|
||||
if last_none_activity:
|
||||
clock_out(request_data)
|
||||
else:
|
||||
clock_in(request_data)
|
||||
|
||||
last_log = punch_data[0]
|
||||
device.last_fetch_date, device.last_fetch_time = (
|
||||
last_log["PunchDate"].date(),
|
||||
last_log["PunchDate"].time(),
|
||||
)
|
||||
device.save()
|
||||
|
||||
return len(punch_data)
|
||||
|
||||
|
||||
def etimeoffice_biometric_attendance_scheduler(device_id):
|
||||
device = BiometricDevices.find(device_id)
|
||||
if device and device.is_scheduler:
|
||||
etimeoffice_biometric_attendance_logs(device)
|
||||
|
||||
|
||||
try:
|
||||
devices = BiometricDevices.objects.all().update(is_live=False)
|
||||
for device in BiometricDevices.objects.filter(is_scheduler=True):
|
||||
@@ -2289,6 +2506,15 @@ try:
|
||||
seconds=str_time_seconds(device.scheduler_duration),
|
||||
)
|
||||
scheduler.start()
|
||||
|
||||
elif device.machine_type == "etimeoffice":
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(
|
||||
lambda: etimeoffice_biometric_attendance_scheduler(device.id),
|
||||
"interval",
|
||||
seconds=str_time_seconds(device.scheduler_duration),
|
||||
)
|
||||
scheduler.start()
|
||||
else:
|
||||
pass
|
||||
except:
|
||||
|
||||
Reference in New Issue
Block a user