debugging expense report
This commit is contained in:
parent
fa0a735548
commit
c8c0620ceb
Binary file not shown.
Binary file not shown.
@ -34,12 +34,14 @@ urlpatterns = [
|
|||||||
path('invoices/payments/', views.customer_payments, name='customer_payments'),
|
path('invoices/payments/', views.customer_payments, name='customer_payments'),
|
||||||
path('invoices/receipt/<int:pk>/', views.customer_payment_receipt, name='customer_payment_receipt'),
|
path('invoices/receipt/<int:pk>/', views.customer_payment_receipt, name='customer_payment_receipt'),
|
||||||
path('invoices/sale-receipt/<int:pk>/', views.sale_receipt, name='sale_receipt'),
|
path('invoices/sale-receipt/<int:pk>/', views.sale_receipt, name='sale_receipt'),
|
||||||
|
path('invoices/download-pdf/<int:pk>/', views.download_invoice_pdf, name='download_invoice_pdf'),
|
||||||
path("invoices/edit/<int:pk>/", views.edit_invoice, name="edit_invoice"),
|
path("invoices/edit/<int:pk>/", views.edit_invoice, name="edit_invoice"),
|
||||||
|
|
||||||
# Quotations
|
# Quotations
|
||||||
path('quotations/', views.quotations, name='quotations'),
|
path('quotations/', views.quotations, name='quotations'),
|
||||||
path('quotations/create/', views.quotation_create, name='quotation_create'),
|
path('quotations/create/', views.quotation_create, name='quotation_create'),
|
||||||
path('quotations/<int:pk>/', views.quotation_detail, name='quotation_detail'),
|
path('quotations/<int:pk>/', views.quotation_detail, name='quotation_detail'),
|
||||||
|
path('quotations/download-pdf/<int:pk>/', views.download_quotation_pdf, name='download_quotation_pdf'),
|
||||||
path('quotations/convert/<int:pk>/', views.convert_quotation_to_invoice, name='convert_quotation_to_invoice'),
|
path('quotations/convert/<int:pk>/', views.convert_quotation_to_invoice, name='convert_quotation_to_invoice'),
|
||||||
path('quotations/delete/<int:pk>/', views.delete_quotation, name='delete_quotation'),
|
path('quotations/delete/<int:pk>/', views.delete_quotation, name='delete_quotation'),
|
||||||
path('api/create-quotation/', views.create_quotation_api, name='create_quotation_api'),
|
path('api/create-quotation/', views.create_quotation_api, name='create_quotation_api'),
|
||||||
@ -153,6 +155,7 @@ urlpatterns = [
|
|||||||
path('purchases/lpo/', views.lpo_list, name='lpo_list'),
|
path('purchases/lpo/', views.lpo_list, name='lpo_list'),
|
||||||
path('purchases/lpo/create/', views.lpo_create, name='lpo_create'),
|
path('purchases/lpo/create/', views.lpo_create, name='lpo_create'),
|
||||||
path('purchases/lpo/<int:pk>/', views.lpo_detail, name='lpo_detail'),
|
path('purchases/lpo/<int:pk>/', views.lpo_detail, name='lpo_detail'),
|
||||||
|
path('purchases/lpo/download-pdf/<int:pk>/', views.download_lpo_pdf, name='download_lpo_pdf'),
|
||||||
path('purchases/lpo/convert/<int:pk>/', views.convert_lpo_to_purchase, name='convert_lpo_to_purchase'),
|
path('purchases/lpo/convert/<int:pk>/', views.convert_lpo_to_purchase, name='convert_lpo_to_purchase'),
|
||||||
path('purchases/lpo/delete/<int:pk>/', views.lpo_delete, name='lpo_delete'),
|
path('purchases/lpo/delete/<int:pk>/', views.lpo_delete, name='lpo_delete'),
|
||||||
path('api/create-lpo/', views.create_lpo_api, name='create_lpo_api'),
|
path('api/create-lpo/', views.create_lpo_api, name='create_lpo_api'),
|
||||||
|
|||||||
@ -1034,8 +1034,33 @@ def expense_report(request):
|
|||||||
if not (request.user.is_staff or request.user.has_perm('core.view_reports')):
|
if not (request.user.is_staff or request.user.has_perm('core.view_reports')):
|
||||||
messages.error(request, _("You do not have permission to view reports."))
|
messages.error(request, _("You do not have permission to view reports."))
|
||||||
return redirect('index')
|
return redirect('index')
|
||||||
|
|
||||||
start_date = request.GET.get('start_date')
|
start_date = request.GET.get('start_date')
|
||||||
end_date = request.GET.get('end_date')
|
end_date = request.GET.get('end_date')
|
||||||
|
category_id = request.GET.get('category')
|
||||||
|
|
||||||
|
expenses = Expense.objects.all().order_by('-date')
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
expenses = expenses.filter(date__gte=start_date)
|
||||||
|
if end_date:
|
||||||
|
expenses = expenses.filter(date__lte=end_date)
|
||||||
|
if category_id:
|
||||||
|
expenses = expenses.filter(category_id=category_id)
|
||||||
|
|
||||||
|
total_amount = expenses.aggregate(Sum('amount'))['amount__sum'] or 0
|
||||||
|
categories = ExpenseCategory.objects.all().order_by('name_en')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'expenses': expenses,
|
||||||
|
'total_amount': total_amount,
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'categories': categories,
|
||||||
|
'category_id': category_id,
|
||||||
|
'settings': SystemSetting.objects.first(),
|
||||||
|
}
|
||||||
|
return render(request, 'core/expense_report.html', context)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_expenses_excel(request):
|
def export_expenses_excel(request):
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
17
hr/forms.py
17
hr/forms.py
@ -1,7 +1,8 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Employee
|
from .models import Employee, Attendance
|
||||||
|
|
||||||
class EmployeeForm(forms.ModelForm):
|
class EmployeeForm(forms.ModelForm):
|
||||||
|
# ... existing code ...
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Employee
|
model = Employee
|
||||||
fields = [
|
fields = [
|
||||||
@ -25,3 +26,17 @@ class EmployeeForm(forms.ModelForm):
|
|||||||
'biometric_id': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Device User ID'}),
|
'biometric_id': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Device User ID'}),
|
||||||
'user': forms.Select(attrs={'class': 'form-select'}),
|
'user': forms.Select(attrs={'class': 'form-select'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AttendanceForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Attendance
|
||||||
|
fields = ['employee', 'date', 'check_in', 'check_out', 'device', 'notes']
|
||||||
|
widgets = {
|
||||||
|
'employee': forms.Select(attrs={'class': 'form-select select2'}),
|
||||||
|
'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
|
'check_in': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
|
||||||
|
'check_out': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
|
||||||
|
'device': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2, 'placeholder': 'Optional notes...'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,39 +2,124 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
<h1 class="h3 mb-0 text-gray-800">{{ title }}</h1>
|
<h1 class="h3 mb-0 text-gray-800">{{ title }}</h1>
|
||||||
|
<a href="{% url 'hr:attendance_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to List" %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
<div class="card-body">
|
<div class="card-body p-4 p-md-5">
|
||||||
<form method="post">
|
<form method="post" class="needs-validation" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form %}
|
|
||||||
<div class="mb-3">
|
<div class="row g-4">
|
||||||
<label class="form-label">{{ field.label }}</label>
|
<!-- Employee Field -->
|
||||||
{{ field }}
|
<div class="col-12">
|
||||||
{% if field.errors %}
|
<label class="form-label fw-bold" for="{{ form.employee.id_for_label }}">
|
||||||
<div class="text-danger small">
|
{{ form.employee.label }}
|
||||||
{{ field.errors }}
|
</label>
|
||||||
|
{{ form.employee }}
|
||||||
|
{% if form.employee.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.employee.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{% trans "Select the employee for this attendance record." %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Field -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold" for="{{ form.date.id_for_label }}">
|
||||||
|
{{ form.date.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.date }}
|
||||||
|
{% if form.date.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.date.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
<!-- Device Field -->
|
||||||
<a href="{% url 'hr:attendance_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold" for="{{ form.device.id_for_label }}">
|
||||||
|
{{ form.device.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.device }}
|
||||||
|
{% if form.device.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.device.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{% trans "Optional: Source biometric device." %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Check In & Out -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold text-success" for="{{ form.check_in.id_for_label }}">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-1"></i> {{ form.check_in.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.check_in }}
|
||||||
|
{% if form.check_in.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.check_in.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold text-danger" for="{{ form.check_out.id_for_label }}">
|
||||||
|
<i class="bi bi-box-arrow-left me-1"></i> {{ form.check_out.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.check_out }}
|
||||||
|
{% if form.check_out.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.check_out.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-bold" for="{{ form.notes.id_for_label }}">
|
||||||
|
{{ form.notes.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.notes }}
|
||||||
|
{% if form.notes.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.notes.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary rounded-3 px-4 py-2">
|
||||||
|
<i class="bi bi-check-circle me-1"></i> {% trans "Save Attendance" %}
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'hr:attendance_list' %}" class="btn btn-light rounded-3 px-4 py-2 text-secondary border">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: #0d6efd;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1);
|
||||||
|
}
|
||||||
|
label.fw-bold {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Add bootstrap classes to form fields if not already there
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
// Initialize Select2 if available
|
||||||
if (!el.classList.contains('form-control') && !el.classList.contains('form-select')) {
|
if (typeof jQuery !== 'undefined' && typeof jQuery.fn.select2 !== 'undefined') {
|
||||||
if (el.tagName === 'SELECT') el.classList.add('form-select');
|
$('.select2').select2({
|
||||||
else el.classList.add('form-control');
|
theme: 'bootstrap-5',
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -2,43 +2,114 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid py-4">
|
||||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
<h1 class="h3 mb-0 text-gray-800">{% trans "Attendance Records" %}</h1>
|
<div>
|
||||||
<a href="{% url 'hr:attendance_add' %}" class="btn btn-primary shadow-sm">
|
<h1 class="h3 mb-1 text-gray-800">{% trans "Attendance Records" %}</h1>
|
||||||
<i class="bi bi-plus-lg me-1"></i> {% trans "Add Attendance" %}
|
<p class="text-muted small mb-0">{% trans "Track and manage employee daily check-ins." %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'hr:sync_all_devices' %}" class="btn btn-outline-info shadow-sm rounded-3">
|
||||||
|
<i class="bi bi-arrow-repeat me-1"></i> {% trans "Sync Biometric Data" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'hr:attendance_add' %}" class="btn btn-primary shadow-sm rounded-3">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> {% trans "Manual Entry" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card shadow mb-4">
|
<!-- Stats Row -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 h-100 py-2">
|
||||||
<div class="card-body">
|
<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 "Today's Present" %}</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ today_count|default:"0" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<i class="bi bi-people-fill fa-2x text-gray-300"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
||||||
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
|
<table class="table table-hover align-middle mb-0" width="100%" cellspacing="0">
|
||||||
<thead class="table-light">
|
<thead class="bg-light text-secondary small text-uppercase">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Date" %}</th>
|
<th class="ps-4">{% trans "Date" %}</th>
|
||||||
<th>{% trans "Employee" %}</th>
|
<th>{% trans "Employee" %}</th>
|
||||||
<th>{% trans "Check In" %}</th>
|
<th>{% trans "Check In" %}</th>
|
||||||
<th>{% trans "Check Out" %}</th>
|
<th>{% trans "Check Out" %}</th>
|
||||||
<th class="text-center">{% trans "Actions" %}</th>
|
<th>{% trans "Device" %}</th>
|
||||||
|
<th class="text-end pe-4">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for att in attendances %}
|
{% for att in attendances %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ att.date }}</td>
|
<td class="ps-4">
|
||||||
<td>{{ att.employee }}</td>
|
<span class="fw-medium text-dark">{{ att.date|date:"d M Y" }}</span>
|
||||||
<td>{{ att.check_in|default:"--" }}</td>
|
</td>
|
||||||
<td>{{ att.check_out|default:"--" }}</td>
|
<td>
|
||||||
<td class="text-center">
|
<div class="d-flex align-items-center">
|
||||||
<a href="{% url 'hr:attendance_edit' att.pk %}" class="btn btn-sm btn-outline-primary">
|
<div class="avatar-sm bg-light rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-person text-secondary"></i>
|
||||||
|
</div>
|
||||||
|
<span class="fw-semibold">{{ att.employee }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if att.check_in %}
|
||||||
|
<span class="badge bg-success-soft text-success">
|
||||||
|
<i class="bi bi-clock me-1"></i> {{ att.check_in|time:"H:i" }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">--</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if att.check_out %}
|
||||||
|
<span class="badge bg-danger-soft text-danger">
|
||||||
|
<i class="bi bi-clock me-1"></i> {{ att.check_out|time:"H:i" }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">--</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-muted small">
|
||||||
|
{% if att.device %}
|
||||||
|
<i class="bi bi-cpu me-1"></i> {{ att.device.name }}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-pencil-square me-1"></i> {% trans "Manual" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<a href="{% url 'hr:attendance_edit' att.pk %}" class="btn btn-sm btn-light rounded-circle shadow-none border" title="{% trans "Edit" %}">
|
||||||
|
<i class="bi bi-pencil text-primary"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center">{% trans "No attendance records found." %}</td>
|
<td colspan="6" class="text-center py-5">
|
||||||
|
<div class="py-4">
|
||||||
|
<i class="bi bi-calendar-x display-1 text-light mb-3"></i>
|
||||||
|
<p class="text-muted">{% trans "No attendance records found." %}</p>
|
||||||
|
<a href="{% url 'hr:attendance_add' %}" class="btn btn-sm btn-primary mt-2">
|
||||||
|
{% trans "Add First Record" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -46,19 +117,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Page navigation" class="mt-4">
|
<div class="px-4 py-3 border-top">
|
||||||
<ul class="pagination justify-content-center">
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">{% trans "Previous" %}</a></li>
|
<li class="page-item">
|
||||||
|
<a class="page-link rounded-start-3" href="?page={{ page_obj.previous_page_number }}">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="page-item disabled"><span class="page-link">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span></li>
|
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link text-dark fw-medium">
|
||||||
|
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">{% trans "Next" %}</a></li>
|
<li class="page-item">
|
||||||
|
<a class="page-link rounded-end-3" href="?page={{ page_obj.next_page_number }}">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-success-soft { background-color: rgba(25, 135, 84, 0.1); }
|
||||||
|
.bg-danger-soft { background-color: rgba(220, 53, 69, 0.1); }
|
||||||
|
.avatar-sm { font-size: 0.8rem; }
|
||||||
|
.table thead th { border-top: 0; }
|
||||||
|
.card { border: none; }
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -17,6 +17,7 @@ urlpatterns = [
|
|||||||
path('attendance/', views.AttendanceListView.as_view(), name='attendance_list'),
|
path('attendance/', views.AttendanceListView.as_view(), name='attendance_list'),
|
||||||
path('attendance/add/', views.AttendanceCreateView.as_view(), name='attendance_add'),
|
path('attendance/add/', views.AttendanceCreateView.as_view(), name='attendance_add'),
|
||||||
path('attendance/<int:pk>/edit/', views.AttendanceUpdateView.as_view(), name='attendance_edit'),
|
path('attendance/<int:pk>/edit/', views.AttendanceUpdateView.as_view(), name='attendance_edit'),
|
||||||
|
path('attendance/sync/', views.sync_all_devices, name='sync_all_devices'),
|
||||||
|
|
||||||
path('leave/', views.LeaveRequestListView.as_view(), name='leave_list'),
|
path('leave/', views.LeaveRequestListView.as_view(), name='leave_list'),
|
||||||
path('leave/add/', views.LeaveRequestCreateView.as_view(), name='leave_add'),
|
path('leave/add/', views.LeaveRequestCreateView.as_view(), name='leave_add'),
|
||||||
|
|||||||
39
hr/views.py
39
hr/views.py
@ -3,7 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .models import Employee, Department, Attendance, LeaveRequest, JobPosition, BiometricDevice
|
from .models import Employee, Department, Attendance, LeaveRequest, JobPosition, BiometricDevice
|
||||||
from .forms import EmployeeForm
|
from .forms import EmployeeForm, AttendanceForm
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -85,9 +85,15 @@ class AttendanceListView(LoginRequiredMixin, ListView):
|
|||||||
context_object_name = 'attendances'
|
context_object_name = 'attendances'
|
||||||
paginate_by = 50
|
paginate_by = 50
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['today_count'] = Attendance.objects.filter(date=timezone.now().date()).count()
|
||||||
|
return context
|
||||||
|
|
||||||
class AttendanceCreateView(LoginRequiredMixin, CreateView):
|
class AttendanceCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
|
||||||
model = Attendance
|
model = Attendance
|
||||||
fields = ['employee', 'date', 'check_in', 'check_out', 'device', 'notes']
|
form_class = AttendanceForm
|
||||||
template_name = 'hr/attendance_form.html'
|
template_name = 'hr/attendance_form.html'
|
||||||
success_url = reverse_lazy('hr:attendance_list')
|
success_url = reverse_lazy('hr:attendance_list')
|
||||||
|
|
||||||
@ -98,7 +104,7 @@ class AttendanceCreateView(LoginRequiredMixin, CreateView):
|
|||||||
|
|
||||||
class AttendanceUpdateView(LoginRequiredMixin, UpdateView):
|
class AttendanceUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
model = Attendance
|
model = Attendance
|
||||||
fields = ['employee', 'date', 'check_in', 'check_out', 'device', 'notes']
|
form_class = AttendanceForm
|
||||||
template_name = 'hr/attendance_form.html'
|
template_name = 'hr/attendance_form.html'
|
||||||
success_url = reverse_lazy('hr:attendance_list')
|
success_url = reverse_lazy('hr:attendance_list')
|
||||||
|
|
||||||
@ -108,6 +114,7 @@ class AttendanceUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
class LeaveRequestListView(LoginRequiredMixin, ListView):
|
class LeaveRequestListView(LoginRequiredMixin, ListView):
|
||||||
|
|
||||||
model = LeaveRequest
|
model = LeaveRequest
|
||||||
template_name = 'hr/leave_list.html'
|
template_name = 'hr/leave_list.html'
|
||||||
context_object_name = 'leaves'
|
context_object_name = 'leaves'
|
||||||
@ -187,3 +194,29 @@ def sync_device_logs(request, pk):
|
|||||||
messages.success(request, _("Sync Successful! Fetched %(total)s records, %(new)s new.") % {'total': result['total'], 'new': result['new']})
|
messages.success(request, _("Sync Successful! Fetched %(total)s records, %(new)s new.") % {'total': result['total'], 'new': result['new']})
|
||||||
|
|
||||||
return redirect('hr:device_list')
|
return redirect('hr:device_list')
|
||||||
|
|
||||||
|
def sync_all_devices(request):
|
||||||
|
devices = BiometricDevice.objects.filter(status='active')
|
||||||
|
if not devices.exists():
|
||||||
|
messages.warning(request, _("No active biometric devices found to sync."))
|
||||||
|
return redirect('hr:attendance_list')
|
||||||
|
|
||||||
|
total_new = 0
|
||||||
|
total_fetched = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
result = device.sync_data()
|
||||||
|
if result.get('error'):
|
||||||
|
errors.append(f"{device.name}: {result['error']}")
|
||||||
|
else:
|
||||||
|
total_new += result['new']
|
||||||
|
total_fetched += result['total']
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
messages.warning(request, _("Sync partially completed. Errors: %(errors)s") % {'errors': ", ".join(errors)})
|
||||||
|
|
||||||
|
if total_fetched > 0 or not errors:
|
||||||
|
messages.success(request, _("Sync completed! Total records: %(total)s, New: %(new)s") % {'total': total_fetched, 'new': total_new})
|
||||||
|
|
||||||
|
return redirect('hr:attendance_list')
|
||||||
Loading…
x
Reference in New Issue
Block a user