adding biometerc
This commit is contained in:
parent
9dfa03d69c
commit
2ad0af108e
Binary file not shown.
Binary file not shown.
@ -61,6 +61,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'core',
|
||||
'accounting',
|
||||
'hr',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@ -9,6 +9,7 @@ urlpatterns = [
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
path("", include("core.urls")),
|
||||
path("accounting/", include("accounting.urls")),
|
||||
path("hr/", include("hr.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
@ -219,6 +219,46 @@
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- HR Group -->
|
||||
<li class="sidebar-group-header mt-2">
|
||||
<a href="#hrSubmenu" data-bs-toggle="collapse" aria-expanded="{% if 'hr/' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
|
||||
<span>{% trans "Human Resources" %}</span>
|
||||
<i class="bi bi-chevron-down chevron"></i>
|
||||
</a>
|
||||
<ul class="collapse list-unstyled sub-menu {% if 'hr/' in path %}show{% endif %}" id="hrSubmenu">
|
||||
<li>
|
||||
<a href="{% url 'hr:dashboard' %}" class="{% if url_name == 'dashboard' and 'hr/' in path %}active{% endif %}">
|
||||
<i class="bi bi-speedometer"></i> {% trans "Overview" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'hr:employee_list' %}" class="{% if 'hr/employees' in path %}active{% endif %}">
|
||||
<i class="bi bi-people-fill"></i> {% trans "Employees" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'hr:department_list' %}" class="{% if 'hr/departments' in path %}active{% endif %}">
|
||||
<i class="bi bi-diagram-3"></i> {% trans "Departments" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'hr:attendance_list' %}" class="{% if 'hr/attendance' in path %}active{% endif %}">
|
||||
<i class="bi bi-clock-history"></i> {% trans "Attendance" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'hr:leave_list' %}" class="{% if 'hr/leave' in path %}active{% endif %}">
|
||||
<i class="bi bi-calendar2-range"></i> {% trans "Leave Requests" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'hr:device_list' %}" class="{% if 'hr/devices' in path %}active{% endif %}">
|
||||
<i class="bi bi-fingerprint"></i> {% trans "Biometric Devices" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Reports Group -->
|
||||
<li class="sidebar-group-header mt-2">
|
||||
<a href="#reportsSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'reports' or url_name == 'customer_statement' or url_name == 'supplier_statement' or url_name == 'cashflow_report' %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
|
||||
|
||||
0
hr/__init__.py
Normal file
0
hr/__init__.py
Normal file
BIN
hr/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
hr/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
hr/__pycache__/admin.cpython-311.pyc
Normal file
BIN
hr/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
hr/__pycache__/apps.cpython-311.pyc
Normal file
BIN
hr/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
hr/__pycache__/models.cpython-311.pyc
Normal file
BIN
hr/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
hr/__pycache__/urls.cpython-311.pyc
Normal file
BIN
hr/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
hr/__pycache__/views.cpython-311.pyc
Normal file
BIN
hr/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
34
hr/admin.py
Normal file
34
hr/admin.py
Normal file
@ -0,0 +1,34 @@
|
||||
from django.contrib import admin
|
||||
from .models import Department, JobPosition, Employee, Attendance, LeaveRequest, BiometricDevice
|
||||
|
||||
@admin.register(Department)
|
||||
class DepartmentAdmin(admin.ModelAdmin):
|
||||
list_display = ('name_en', 'name_ar')
|
||||
|
||||
@admin.register(JobPosition)
|
||||
class JobPositionAdmin(admin.ModelAdmin):
|
||||
list_display = ('title_en', 'title_ar', 'department')
|
||||
list_filter = ('department',)
|
||||
|
||||
@admin.register(Employee)
|
||||
class EmployeeAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'last_name', 'email', 'department', 'job_position', 'status')
|
||||
list_filter = ('status', 'department', 'gender')
|
||||
search_fields = ('first_name', 'last_name', 'email', 'phone')
|
||||
|
||||
@admin.register(Attendance)
|
||||
class AttendanceAdmin(admin.ModelAdmin):
|
||||
list_display = ('employee', 'date', 'check_in', 'check_out', 'device')
|
||||
list_filter = ('date', 'employee', 'device')
|
||||
|
||||
@admin.register(LeaveRequest)
|
||||
class LeaveRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('employee', 'leave_type', 'start_date', 'end_date', 'status')
|
||||
list_filter = ('status', 'leave_type')
|
||||
list_editable = ('status',)
|
||||
|
||||
@admin.register(BiometricDevice)
|
||||
class BiometricDeviceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'ip_address', 'port', 'device_type', 'status', 'last_sync')
|
||||
list_filter = ('status', 'device_type')
|
||||
search_fields = ('name', 'ip_address')
|
||||
6
hr/apps.py
Normal file
6
hr/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HrConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'hr'
|
||||
89
hr/migrations/0001_initial.py
Normal file
89
hr/migrations/0001_initial.py
Normal file
@ -0,0 +1,89 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-05 13:13
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Department',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name_en', models.CharField(max_length=100, verbose_name='Name (English)')),
|
||||
('name_ar', models.CharField(max_length=100, verbose_name='Name (Arabic)')),
|
||||
('description', models.TextField(blank=True, verbose_name='Description')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Employee',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=100, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=100, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||
('phone', models.CharField(max_length=20, verbose_name='Phone Number')),
|
||||
('gender', models.CharField(choices=[('M', 'Male'), ('F', 'Female')], default='M', max_length=1, verbose_name='Gender')),
|
||||
('hire_date', models.DateField(default=django.utils.timezone.now, verbose_name='Hire Date')),
|
||||
('salary', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Basic Salary')),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('on_leave', 'On Leave'), ('terminated', 'Terminated'), ('resigned', 'Resigned')], default='active', max_length=20, verbose_name='Status')),
|
||||
('address', models.TextField(blank=True, verbose_name='Address')),
|
||||
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employees', to='hr.department', verbose_name='Department')),
|
||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_profile', to=settings.AUTH_USER_MODEL, verbose_name='User Account')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Attendance',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Date')),
|
||||
('check_in', models.TimeField(blank=True, null=True, verbose_name='Check In')),
|
||||
('check_out', models.TimeField(blank=True, null=True, verbose_name='Check Out')),
|
||||
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendances', to='hr.employee', verbose_name='Employee')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobPosition',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title_en', models.CharField(max_length=100, verbose_name='Job Title (English)')),
|
||||
('title_ar', models.CharField(max_length=100, verbose_name='Job Title (Arabic)')),
|
||||
('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='positions', to='hr.department', verbose_name='Department')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employee',
|
||||
name='job_position',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employees', to='hr.jobposition', verbose_name='Job Position'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeaveRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('leave_type', models.CharField(choices=[('annual', 'Annual Leave'), ('sick', 'Sick Leave'), ('unpaid', 'Unpaid Leave'), ('maternity', 'Maternity Leave'), ('other', 'Other')], default='annual', max_length=20, verbose_name='Leave Type')),
|
||||
('start_date', models.DateField(verbose_name='Start Date')),
|
||||
('end_date', models.DateField(verbose_name='End Date')),
|
||||
('reason', models.TextField(blank=True, verbose_name='Reason')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20, verbose_name='Status')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_leaves', to=settings.AUTH_USER_MODEL, verbose_name='Approved By')),
|
||||
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leave_requests', to='hr.employee', verbose_name='Employee')),
|
||||
],
|
||||
),
|
||||
]
|
||||
33
hr/migrations/0002_biometricdevice_attendance_device.py
Normal file
33
hr/migrations/0002_biometricdevice_attendance_device.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-05 13:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hr', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BiometricDevice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Device Name')),
|
||||
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
|
||||
('port', models.PositiveIntegerField(default=4370, verbose_name='Port')),
|
||||
('device_type', models.CharField(choices=[('zkteco', 'ZKTeco'), ('other', 'Other')], default='zkteco', max_length=20, verbose_name='Device Type')),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive')], default='active', max_length=20, verbose_name='Status')),
|
||||
('last_sync', models.DateTimeField(blank=True, null=True, verbose_name='Last Sync')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='attendance',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='attendances', to='hr.biometricdevice', verbose_name='Source Device'),
|
||||
),
|
||||
]
|
||||
22
hr/migrations/0003_employee_biometric_id_and_more.py
Normal file
22
hr/migrations/0003_employee_biometric_id_and_more.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-05 13:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hr', '0002_biometricdevice_attendance_device'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='employee',
|
||||
name='biometric_id',
|
||||
field=models.IntegerField(blank=True, help_text='The User ID used on the attendance device', null=True, verbose_name='Biometric Device ID'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='attendance',
|
||||
unique_together={('employee', 'date')},
|
||||
),
|
||||
]
|
||||
0
hr/migrations/__init__.py
Normal file
0
hr/migrations/__init__.py
Normal file
BIN
hr/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
hr/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
hr/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
hr/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
211
hr/models.py
Normal file
211
hr/models.py
Normal file
@ -0,0 +1,211 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
import datetime
|
||||
|
||||
class Department(models.Model):
|
||||
name_en = models.CharField(_("Name (English)"), max_length=100)
|
||||
name_ar = models.CharField(_("Name (Arabic)"), max_length=100)
|
||||
description = models.TextField(_("Description"), blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name_en} / {self.name_ar}"
|
||||
|
||||
class JobPosition(models.Model):
|
||||
title_en = models.CharField(_("Job Title (English)"), max_length=100)
|
||||
title_ar = models.CharField(_("Job Title (Arabic)"), max_length=100)
|
||||
department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name='positions', verbose_name=_("Department"))
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title_en} / {self.title_ar}"
|
||||
|
||||
class Employee(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('active', _('Active')),
|
||||
('on_leave', _('On Leave')),
|
||||
('terminated', _('Terminated')),
|
||||
('resigned', _('Resigned')),
|
||||
]
|
||||
GENDER_CHOICES = [
|
||||
('M', _('Male')),
|
||||
('F', _('Female')),
|
||||
]
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='employee_profile', verbose_name=_("User Account"))
|
||||
first_name = models.CharField(_("First Name"), max_length=100)
|
||||
last_name = models.CharField(_("Last Name"), max_length=100)
|
||||
email = models.EmailField(_("Email"), unique=True)
|
||||
phone = models.CharField(_("Phone Number"), max_length=20)
|
||||
gender = models.CharField(_("Gender"), max_length=1, choices=GENDER_CHOICES, default='M')
|
||||
|
||||
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, related_name='employees', verbose_name=_("Department"))
|
||||
job_position = models.ForeignKey(JobPosition, on_delete=models.SET_NULL, null=True, blank=True, related_name='employees', verbose_name=_("Job Position"))
|
||||
|
||||
hire_date = models.DateField(_("Hire Date"), default=timezone.now)
|
||||
salary = models.DecimalField(_("Basic Salary"), max_digits=10, decimal_places=2, default=0)
|
||||
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
|
||||
address = models.TextField(_("Address"), blank=True)
|
||||
date_of_birth = models.DateField(_("Date of Birth"), null=True, blank=True)
|
||||
|
||||
# New field for linking with biometric device
|
||||
biometric_id = models.IntegerField(_("Biometric Device ID"), null=True, blank=True, help_text=_("The User ID used on the attendance device"))
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
class BiometricDevice(models.Model):
|
||||
DEVICE_TYPES = [
|
||||
('zkteco', _('ZKTeco')),
|
||||
('other', _('Other')),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('active', _('Active')),
|
||||
('inactive', _('Inactive')),
|
||||
]
|
||||
|
||||
name = models.CharField(_("Device Name"), max_length=100)
|
||||
ip_address = models.GenericIPAddressField(_("IP Address"))
|
||||
port = models.PositiveIntegerField(_("Port"), default=4370)
|
||||
device_type = models.CharField(_("Device Type"), max_length=20, choices=DEVICE_TYPES, default='zkteco')
|
||||
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
last_sync = models.DateTimeField(_("Last Sync"), null=True, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.ip_address})"
|
||||
|
||||
def sync_data(self):
|
||||
"""
|
||||
Connects to the device and fetches attendance records.
|
||||
Returns a summary dictionary: {'new': count, 'total': count, 'error': str}
|
||||
"""
|
||||
if self.device_type != 'zkteco':
|
||||
return {'error': 'Only ZKTeco devices are supported for auto-sync currently.'}
|
||||
|
||||
from zk import ZK
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = ZK(self.ip_address, port=self.port, timeout=10)
|
||||
conn.connect()
|
||||
# self.status = 'active' # Update status on successful connect
|
||||
|
||||
attendance_list = conn.get_attendance()
|
||||
|
||||
new_records = 0
|
||||
|
||||
for att in attendance_list:
|
||||
user_id = att.user_id # String or Int depending on device
|
||||
timestamp = att.timestamp
|
||||
|
||||
try:
|
||||
# Find employee with this biometric ID
|
||||
# We cast user_id to int to be safe if model is int
|
||||
emp_id = int(user_id)
|
||||
employee = Employee.objects.filter(biometric_id=emp_id).first()
|
||||
|
||||
if employee:
|
||||
date = timestamp.date()
|
||||
time = timestamp.time()
|
||||
|
||||
# Get or create attendance record for this day
|
||||
attendance, created = Attendance.objects.get_or_create(
|
||||
employee=employee,
|
||||
date=date,
|
||||
defaults={'device': self}
|
||||
)
|
||||
|
||||
updated = False
|
||||
# Logic: First punch is check_in, Last punch is check_out
|
||||
if attendance.check_in is None:
|
||||
attendance.check_in = time
|
||||
updated = True
|
||||
elif time < attendance.check_in:
|
||||
# If we found an earlier time, update check_in
|
||||
attendance.check_in = time
|
||||
updated = True
|
||||
|
||||
if attendance.check_out is None and time > attendance.check_in:
|
||||
attendance.check_out = time
|
||||
updated = True
|
||||
elif attendance.check_out and time > attendance.check_out:
|
||||
attendance.check_out = time
|
||||
updated = True
|
||||
|
||||
if created:
|
||||
new_records += 1
|
||||
elif updated:
|
||||
attendance.save()
|
||||
|
||||
except ValueError:
|
||||
continue # Skip if user_id is not parseable
|
||||
except Exception as e:
|
||||
print(f"Error processing record: {e}")
|
||||
continue
|
||||
|
||||
self.last_sync = timezone.now()
|
||||
self.status = 'active'
|
||||
self.save()
|
||||
|
||||
return {'new': new_records, 'total': len(attendance_list), 'error': None}
|
||||
|
||||
except Exception as e:
|
||||
self.status = 'inactive'
|
||||
self.save()
|
||||
return {'error': str(e)}
|
||||
finally:
|
||||
if conn:
|
||||
conn.disconnect()
|
||||
|
||||
class Attendance(models.Model):
|
||||
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='attendances', verbose_name=_("Employee"))
|
||||
date = models.DateField(_("Date"), default=timezone.now)
|
||||
check_in = models.TimeField(_("Check In"), null=True, blank=True)
|
||||
check_out = models.TimeField(_("Check Out"), null=True, blank=True)
|
||||
notes = models.TextField(_("Notes"), blank=True)
|
||||
device = models.ForeignKey(BiometricDevice, on_delete=models.SET_NULL, null=True, blank=True, related_name='attendances', verbose_name=_("Source Device"))
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date']
|
||||
unique_together = ['employee', 'date'] # Prevent duplicate rows per day per employee
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.employee} - {self.date}"
|
||||
|
||||
class LeaveRequest(models.Model):
|
||||
LEAVE_TYPES = [
|
||||
('annual', _('Annual Leave')),
|
||||
('sick', _('Sick Leave')),
|
||||
('unpaid', _('Unpaid Leave')),
|
||||
('maternity', _('Maternity Leave')),
|
||||
('other', _('Other')),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('pending', _('Pending')),
|
||||
('approved', _('Approved')),
|
||||
('rejected', _('Rejected')),
|
||||
]
|
||||
|
||||
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='leave_requests', verbose_name=_("Employee"))
|
||||
leave_type = models.CharField(_("Leave Type"), max_length=20, choices=LEAVE_TYPES, default='annual')
|
||||
start_date = models.DateField(_("Start Date"))
|
||||
end_date = models.DateField(_("End Date"))
|
||||
reason = models.TextField(_("Reason"), blank=True)
|
||||
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||
approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_leaves', verbose_name=_("Approved By"))
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.employee} ({self.start_date} to {self.end_date})"
|
||||
|
||||
@property
|
||||
def duration_days(self):
|
||||
return (self.end_date - self.start_date).days + 1
|
||||
41
hr/templates/hr/attendance_list.html
Normal file
41
hr/templates/hr/attendance_list.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">{% trans "Attendance Records" %}</h1>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Employee" %}</th>
|
||||
<th>{% trans "Check In" %}</th>
|
||||
<th>{% trans "Check Out" %}</th>
|
||||
<th>{% trans "Notes" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for attendance in attendances %}
|
||||
<tr>
|
||||
<td>{{ attendance.date }}</td>
|
||||
<td>{{ attendance.employee }}</td>
|
||||
<td>{{ attendance.check_in|default:"-" }}</td>
|
||||
<td>{{ attendance.check_out|default:"-" }}</td>
|
||||
<td>{{ attendance.notes }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">{% trans "No records found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
hr/templates/hr/biometric_device_confirm_delete.html
Normal file
21
hr/templates/hr/biometric_device_confirm_delete.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">{% trans "Delete Device" %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<p>{% trans "Are you sure you want to delete the device" %} "<strong>{{ object.name }}</strong>"?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">{% trans "Confirm Delete" %}</button>
|
||||
<a href="{% url 'hr:device_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
61
hr/templates/hr/biometric_device_form.html
Normal file
61
hr/templates/hr/biometric_device_form.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">{{ title }}</h1>
|
||||
<a href="{% url 'hr:device_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">{% trans "Device Name" %}</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ form.name.value|default:'' }}" required>
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">{% trans "IP Address" %}</label>
|
||||
<input type="text" name="ip_address" class="form-control" value="{{ form.ip_address.value|default:'' }}" required placeholder="192.168.1.201">
|
||||
{% if form.ip_address.errors %}
|
||||
<div class="text-danger small">{{ form.ip_address.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">{% trans "Port" %}</label>
|
||||
<input type="number" name="port" class="form-control" value="{{ form.port.value|default:'4370' }}" required>
|
||||
{% if form.port.errors %}
|
||||
<div class="text-danger small">{{ form.port.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">{% trans "Device Type" %}</label>
|
||||
<select name="device_type" class="form-select">
|
||||
{% for code, name in form.fields.device_type.choices %}
|
||||
<option value="{{ code }}" {% if form.device_type.value == code %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">{% trans "Status" %}</label>
|
||||
<select name="status" class="form-select">
|
||||
{% for code, name in form.fields.status.choices %}
|
||||
<option value="{{ code }}" {% if form.status.value == code %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save Device" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
80
hr/templates/hr/biometric_device_list.html
Normal file
80
hr/templates/hr/biometric_device_list.html
Normal file
@ -0,0 +1,80 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">{% trans "Biometric Devices" %}</h1>
|
||||
<a href="{% url 'hr:device_add' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg"></i> {% trans "Add Device" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
<div class="messages mb-4">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Device Name" %}</th>
|
||||
<th>{% trans "IP Address" %}</th>
|
||||
<th>{% trans "Port" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Last Sync" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for device in devices %}
|
||||
<tr>
|
||||
<td>{{ device.name }}</td>
|
||||
<td>{{ device.ip_address }}</td>
|
||||
<td>{{ device.port }}</td>
|
||||
<td>{{ device.get_device_type_display }}</td>
|
||||
<td>
|
||||
{% if device.status == 'active' %}
|
||||
<span class="badge bg-success">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ device.last_sync|default:"-" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'hr:device_test' device.pk %}" class="btn btn-sm btn-info text-white" title="{% trans 'Test Connection' %}">
|
||||
<i class="bi bi-broadcast"></i>
|
||||
</a>
|
||||
<a href="{% url 'hr:device_sync' device.pk %}" class="btn btn-sm btn-success" title="{% trans 'Sync Logs' %}">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</a>
|
||||
<a href="{% url 'hr:device_edit' device.pk %}" class="btn btn-sm btn-primary" title="{% trans 'Edit' %}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'hr:device_delete' device.pk %}" class="btn btn-sm btn-danger" title="{% trans 'Delete' %}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">{% trans "No biometric devices configured." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
78
hr/templates/hr/dashboard.html
Normal file
78
hr/templates/hr/dashboard.html
Normal file
@ -0,0 +1,78 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">{% trans "HR Dashboard" %}</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||
{% trans "Total Employees" %}</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ total_employees }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-users fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
||||
{% trans "Active Employees" %}</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ active_employees }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-user-check fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||
{% trans "Departments" %}</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ departments_count }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-building fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||
{% trans "Pending Leave Requests" %}</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ pending_leaves }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-calendar-minus fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
hr/templates/hr/department_list.html
Normal file
37
hr/templates/hr/department_list.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">{% trans "Departments" %}</h1>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name (English)" %}</th>
|
||||
<th>{% trans "Name (Arabic)" %}</th>
|
||||
<th>{% trans "Employees" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for department in departments %}
|
||||
<tr>
|
||||
<td>{{ department.name_en }}</td>
|
||||
<td>{{ department.name_ar }}</td>
|
||||
<td>{{ department.employees.count }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">{% trans "No departments found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
46
hr/templates/hr/employee_detail.html
Normal file
46
hr/templates/hr/employee_detail.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">{{ employee.first_name }} {{ employee.last_name }}</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">{% trans "Personal Information" %}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>{% trans "Email" %}:</strong> {{ employee.email }}</p>
|
||||
<p><strong>{% trans "Phone" %}:</strong> {{ employee.phone }}</p>
|
||||
<p><strong>{% trans "Gender" %}:</strong> {{ employee.get_gender_display }}</p>
|
||||
<p><strong>{% trans "Date of Birth" %}:</strong> {{ employee.date_of_birth|default:"-" }}</p>
|
||||
<p><strong>{% trans "Address" %}:</strong> {{ employee.address|default:"-" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">{% trans "Job Details" %}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>{% trans "Department" %}:</strong> {{ employee.department.name_en }} / {{ employee.department.name_ar }}</p>
|
||||
<p><strong>{% trans "Position" %}:</strong> {{ employee.job_position.title_en }} / {{ employee.job_position.title_ar }}</p>
|
||||
<p><strong>{% trans "Hire Date" %}:</strong> {{ employee.hire_date }}</p>
|
||||
<p><strong>{% trans "Salary" %}:</strong> {{ employee.salary }}</p>
|
||||
<p><strong>{% trans "Status" %}:</strong> {{ employee.get_status_display }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="{% url 'hr:employee_edit' employee.pk %}" class="btn btn-primary">{% trans "Edit Employee" %}</a>
|
||||
<a href="{% url 'hr:employee_list' %}" class="btn btn-secondary">{% trans "Back to List" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
hr/templates/hr/employee_form.html
Normal file
19
hr/templates/hr/employee_form.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">{{ title }}</h1>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
||||
<a href="{% url 'hr:employee_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
62
hr/templates/hr/employee_list.html
Normal file
62
hr/templates/hr/employee_list.html
Normal file
@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">{% trans "Employees" %}</h1>
|
||||
<a href="{% url 'hr:employee_add' %}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
|
||||
<i class="fas fa-plus fa-sm text-white-50"></i> {% trans "Add Employee" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Department" %}</th>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Phone" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for employee in employees %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'hr:employee_detail' employee.pk %}">
|
||||
{{ employee.first_name }} {{ employee.last_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ employee.department.name_en }} / {{ employee.department.name_ar }}</td>
|
||||
<td>{{ employee.job_position.title_en }} / {{ employee.job_position.title_ar }}</td>
|
||||
<td>{{ employee.email }}</td>
|
||||
<td>{{ employee.phone }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{% if employee.status == 'active' %}success{% elif employee.status == 'terminated' %}danger{% else %}warning{% endif %}">
|
||||
{{ employee.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'hr:employee_edit' employee.pk %}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">{% trans "No employees found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
hr/templates/hr/leave_form.html
Normal file
19
hr/templates/hr/leave_form.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">{% trans "New Leave Request" %}</h1>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Submit Request" %}</button>
|
||||
<a href="{% url 'hr:leave_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
hr/templates/hr/leave_list.html
Normal file
52
hr/templates/hr/leave_list.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">{% trans "Leave Requests" %}</h1>
|
||||
<a href="{% url 'hr:leave_add' %}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
|
||||
<i class="fas fa-plus fa-sm text-white-50"></i> {% trans "New Request" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Employee" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Start Date" %}</th>
|
||||
<th>{% trans "End Date" %}</th>
|
||||
<th>{% trans "Duration (Days)" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for leave in leaves %}
|
||||
<tr>
|
||||
<td>{{ leave.employee }}</td>
|
||||
<td>{{ leave.get_leave_type_display }}</td>
|
||||
<td>{{ leave.start_date }}</td>
|
||||
<td>{{ leave.end_date }}</td>
|
||||
<td>{{ leave.duration_days }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{% if leave.status == 'approved' %}success{% elif leave.status == 'rejected' %}danger{% else %}warning{% endif %}">
|
||||
{{ leave.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No leave requests found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
3
hr/tests.py
Normal file
3
hr/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
24
hr/urls.py
Normal file
24
hr/urls.py
Normal file
@ -0,0 +1,24 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'hr'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.HRDashboardView.as_view(), name='dashboard'),
|
||||
path('employees/', views.EmployeeListView.as_view(), name='employee_list'),
|
||||
path('employees/add/', views.EmployeeCreateView.as_view(), name='employee_add'),
|
||||
path('employees/<int:pk>/', views.EmployeeDetailView.as_view(), name='employee_detail'),
|
||||
path('employees/<int:pk>/edit/', views.EmployeeUpdateView.as_view(), name='employee_edit'),
|
||||
path('departments/', views.DepartmentListView.as_view(), name='department_list'),
|
||||
path('attendance/', views.AttendanceListView.as_view(), name='attendance_list'),
|
||||
path('leave/', views.LeaveRequestListView.as_view(), name='leave_list'),
|
||||
path('leave/add/', views.LeaveRequestCreateView.as_view(), name='leave_add'),
|
||||
|
||||
# Biometric Devices
|
||||
path('devices/', views.BiometricDeviceListView.as_view(), name='device_list'),
|
||||
path('devices/add/', views.BiometricDeviceCreateView.as_view(), name='device_add'),
|
||||
path('devices/<int:pk>/edit/', views.BiometricDeviceUpdateView.as_view(), name='device_edit'),
|
||||
path('devices/<int:pk>/delete/', views.BiometricDeviceDeleteView.as_view(), name='device_delete'),
|
||||
path('devices/<int:pk>/test/', views.test_device_connection, name='device_test'),
|
||||
path('devices/<int:pk>/sync/', views.sync_device_logs, name='device_sync'),
|
||||
]
|
||||
144
hr/views.py
Normal file
144
hr/views.py
Normal file
@ -0,0 +1,144 @@
|
||||
from django.views.generic import ListView, CreateView, UpdateView, TemplateView, DetailView, DeleteView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import Employee, Department, Attendance, LeaveRequest, JobPosition, BiometricDevice
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.contrib import messages
|
||||
import socket
|
||||
from django.utils import timezone
|
||||
|
||||
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'hr/dashboard.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['total_employees'] = Employee.objects.count()
|
||||
context['active_employees'] = Employee.objects.filter(status='active').count()
|
||||
context['departments_count'] = Department.objects.count()
|
||||
context['pending_leaves'] = LeaveRequest.objects.filter(status='pending').count()
|
||||
return context
|
||||
|
||||
class EmployeeListView(LoginRequiredMixin, ListView):
|
||||
model = Employee
|
||||
template_name = 'hr/employee_list.html'
|
||||
context_object_name = 'employees'
|
||||
|
||||
class EmployeeCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Employee
|
||||
fields = '__all__'
|
||||
template_name = 'hr/employee_form.html'
|
||||
success_url = reverse_lazy('hr:employee_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Add New Employee")
|
||||
return context
|
||||
|
||||
class EmployeeUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Employee
|
||||
fields = '__all__'
|
||||
template_name = 'hr/employee_form.html'
|
||||
success_url = reverse_lazy('hr:employee_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Edit Employee")
|
||||
return context
|
||||
|
||||
class EmployeeDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Employee
|
||||
template_name = 'hr/employee_detail.html'
|
||||
|
||||
class DepartmentListView(LoginRequiredMixin, ListView):
|
||||
model = Department
|
||||
template_name = 'hr/department_list.html'
|
||||
context_object_name = 'departments'
|
||||
|
||||
class AttendanceListView(LoginRequiredMixin, ListView):
|
||||
model = Attendance
|
||||
template_name = 'hr/attendance_list.html'
|
||||
context_object_name = 'attendances'
|
||||
paginate_by = 50
|
||||
|
||||
class LeaveRequestListView(LoginRequiredMixin, ListView):
|
||||
model = LeaveRequest
|
||||
template_name = 'hr/leave_list.html'
|
||||
context_object_name = 'leaves'
|
||||
|
||||
class LeaveRequestCreateView(LoginRequiredMixin, CreateView):
|
||||
model = LeaveRequest
|
||||
fields = ['employee', 'leave_type', 'start_date', 'end_date', 'reason']
|
||||
template_name = 'hr/leave_form.html'
|
||||
success_url = reverse_lazy('hr:leave_list')
|
||||
|
||||
class BiometricDeviceListView(LoginRequiredMixin, ListView):
|
||||
model = BiometricDevice
|
||||
template_name = 'hr/biometric_device_list.html'
|
||||
context_object_name = 'devices'
|
||||
|
||||
class BiometricDeviceCreateView(LoginRequiredMixin, CreateView):
|
||||
model = BiometricDevice
|
||||
fields = ['name', 'ip_address', 'port', 'device_type', 'status']
|
||||
template_name = 'hr/biometric_device_form.html'
|
||||
success_url = reverse_lazy('hr:device_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Add Biometric Device")
|
||||
return context
|
||||
|
||||
class BiometricDeviceUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = BiometricDevice
|
||||
fields = ['name', 'ip_address', 'port', 'device_type', 'status']
|
||||
template_name = 'hr/biometric_device_form.html'
|
||||
success_url = reverse_lazy('hr:device_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Edit Biometric Device")
|
||||
return context
|
||||
|
||||
class BiometricDeviceDeleteView(LoginRequiredMixin, DeleteView):
|
||||
model = BiometricDevice
|
||||
template_name = 'hr/biometric_device_confirm_delete.html'
|
||||
success_url = reverse_lazy('hr:device_list')
|
||||
|
||||
def test_device_connection(request, pk):
|
||||
device = get_object_or_404(BiometricDevice, pk=pk)
|
||||
|
||||
try:
|
||||
# Simple socket connect to see if port is open (timeout 3s)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(3)
|
||||
result = sock.connect_ex((device.ip_address, device.port))
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
messages.success(request, _("Connection successful! Device is online."))
|
||||
device.status = 'active'
|
||||
device.last_sync = timezone.now()
|
||||
device.save()
|
||||
else:
|
||||
# If failed, we still report it but since this is often run in dev/cloud
|
||||
# where the device isn't reachable, we warn.
|
||||
messages.warning(request, _("Connection failed: Port closed or host unreachable. (Error Code: %(code)s)") % {'code': result})
|
||||
device.status = 'inactive'
|
||||
device.save()
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, _("Connection Error: %(error)s") % {'error': str(e)})
|
||||
|
||||
return redirect('hr:device_list')
|
||||
|
||||
def sync_device_logs(request, pk):
|
||||
device = get_object_or_404(BiometricDevice, pk=pk)
|
||||
result = device.sync_data() # This method we added to the model
|
||||
|
||||
if result.get('error'):
|
||||
messages.error(request, _("Sync Failed: %(error)s") % {'error': result['error']})
|
||||
else:
|
||||
messages.success(request, _("Sync Successful! Fetched %(total)s records, %(new)s new.") % {'total': result['total'], 'new': result['new']})
|
||||
|
||||
return redirect('hr:device_list')
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
pyzk==0.9
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user