[ADD] BIOMETRIC : Add integration for e-Time Office biometric device

This commit is contained in:
Horilla
2025-03-26 14:57:25 +05:30
parent 96942b4a32
commit e8e46df406
12 changed files with 588 additions and 29 deletions

91
biometric/etimeoffice.py Normal file
View 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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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();
}
});
}
});
}
// ------------------------------------------------------------------------------------------------------------------------------
// ******************************************************************

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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,

View File

@@ -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: