diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index accc30c..b31ede3 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index f515055..6b2a9ed 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/urls.py b/core/urls.py index 8411960..3d6aaa9 100644 --- a/core/urls.py +++ b/core/urls.py @@ -34,12 +34,14 @@ urlpatterns = [ path('invoices/payments/', views.customer_payments, name='customer_payments'), path('invoices/receipt//', views.customer_payment_receipt, name='customer_payment_receipt'), path('invoices/sale-receipt//', views.sale_receipt, name='sale_receipt'), + path('invoices/download-pdf//', views.download_invoice_pdf, name='download_invoice_pdf'), path("invoices/edit//", views.edit_invoice, name="edit_invoice"), # Quotations path('quotations/', views.quotations, name='quotations'), path('quotations/create/', views.quotation_create, name='quotation_create'), path('quotations//', views.quotation_detail, name='quotation_detail'), + path('quotations/download-pdf//', views.download_quotation_pdf, name='download_quotation_pdf'), path('quotations/convert//', views.convert_quotation_to_invoice, name='convert_quotation_to_invoice'), path('quotations/delete//', views.delete_quotation, name='delete_quotation'), 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/create/', views.lpo_create, name='lpo_create'), path('purchases/lpo//', views.lpo_detail, name='lpo_detail'), + path('purchases/lpo/download-pdf//', views.download_lpo_pdf, name='download_lpo_pdf'), path('purchases/lpo/convert//', views.convert_lpo_to_purchase, name='convert_lpo_to_purchase'), path('purchases/lpo/delete//', views.lpo_delete, name='lpo_delete'), path('api/create-lpo/', views.create_lpo_api, name='create_lpo_api'), diff --git a/core/views.py b/core/views.py index 4bfb7ba..d261d7c 100644 --- a/core/views.py +++ b/core/views.py @@ -1034,8 +1034,33 @@ def expense_report(request): 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.")) return redirect('index') + start_date = request.GET.get('start_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 def export_expenses_excel(request): diff --git a/hr/__pycache__/forms.cpython-311.pyc b/hr/__pycache__/forms.cpython-311.pyc index d17db60..61b6823 100644 Binary files a/hr/__pycache__/forms.cpython-311.pyc and b/hr/__pycache__/forms.cpython-311.pyc differ diff --git a/hr/__pycache__/urls.cpython-311.pyc b/hr/__pycache__/urls.cpython-311.pyc index b501e4d..d9be205 100644 Binary files a/hr/__pycache__/urls.cpython-311.pyc and b/hr/__pycache__/urls.cpython-311.pyc differ diff --git a/hr/__pycache__/views.cpython-311.pyc b/hr/__pycache__/views.cpython-311.pyc index 01c6405..121ae91 100644 Binary files a/hr/__pycache__/views.cpython-311.pyc and b/hr/__pycache__/views.cpython-311.pyc differ diff --git a/hr/forms.py b/hr/forms.py index 7b025dc..657ea97 100644 --- a/hr/forms.py +++ b/hr/forms.py @@ -1,7 +1,8 @@ from django import forms -from .models import Employee +from .models import Employee, Attendance class EmployeeForm(forms.ModelForm): + # ... existing code ... class Meta: model = Employee fields = [ @@ -25,3 +26,17 @@ class EmployeeForm(forms.ModelForm): 'biometric_id': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Device User ID'}), '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...'}), + } + diff --git a/hr/templates/hr/attendance_form.html b/hr/templates/hr/attendance_form.html index b69c42a..5be5f09 100644 --- a/hr/templates/hr/attendance_form.html +++ b/hr/templates/hr/attendance_form.html @@ -2,40 +2,125 @@ {% load i18n %} {% block content %} -
-
-

{{ title }}

-
+
+
+
+ -
-
-
- {% csrf_token %} - {% for field in form %} -
- - {{ field }} - {% if field.errors %} -
- {{ field.errors }} -
- {% endif %} +
+
+ + {% csrf_token %} + +
+ +
+ + {{ form.employee }} + {% if form.employee.errors %} +
{{ form.employee.errors }}
+ {% endif %} +
{% trans "Select the employee for this attendance record." %}
+
+ + +
+ + {{ form.date }} + {% if form.date.errors %} +
{{ form.date.errors }}
+ {% endif %} +
+ + +
+ + {{ form.device }} + {% if form.device.errors %} +
{{ form.device.errors }}
+ {% endif %} +
{% trans "Optional: Source biometric device." %}
+
+ + +
+ + {{ form.check_in }} + {% if form.check_in.errors %} +
{{ form.check_in.errors }}
+ {% endif %} +
+ +
+ + {{ form.check_out }} + {% if form.check_out.errors %} +
{{ form.check_out.errors }}
+ {% endif %} +
+ + +
+ + {{ form.notes }} + {% if form.notes.errors %} +
{{ form.notes.errors }}
+ {% endif %} +
+
+ +
+ + + {% trans "Cancel" %} + +
+
- {% endfor %} - - {% trans "Cancel" %} - +
+ + -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/hr/templates/hr/attendance_list.html b/hr/templates/hr/attendance_list.html index afbc3c7..6dc342a 100644 --- a/hr/templates/hr/attendance_list.html +++ b/hr/templates/hr/attendance_list.html @@ -2,43 +2,114 @@ {% load i18n %} {% block content %} -
+
-

{% trans "Attendance Records" %}

- - {% trans "Add Attendance" %} - +
+

{% trans "Attendance Records" %}

+

{% trans "Track and manage employee daily check-ins." %}

+
+
-
-
+ +
+
+
+
+
+
+
+ {% trans "Today's Present" %}
+
{{ today_count|default:"0" }}
+
+
+ +
+
+
+
+
+
+ +
+
- - +
+ - + - + + {% for att in attendances %} - - - - - + + + + + {% empty %} - + {% endfor %} @@ -46,19 +117,43 @@ {% if is_paginated %} - +
+ +
{% endif %} -{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/hr/urls.py b/hr/urls.py index b46d620..8d6f539 100644 --- a/hr/urls.py +++ b/hr/urls.py @@ -17,6 +17,7 @@ urlpatterns = [ path('attendance/', views.AttendanceListView.as_view(), name='attendance_list'), path('attendance/add/', views.AttendanceCreateView.as_view(), name='attendance_add'), path('attendance//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/add/', views.LeaveRequestCreateView.as_view(), name='leave_add'), diff --git a/hr/views.py b/hr/views.py index e219216..9406089 100644 --- a/hr/views.py +++ b/hr/views.py @@ -3,7 +3,7 @@ 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 .forms import EmployeeForm +from .forms import EmployeeForm, AttendanceForm from django.db.models import Count from django.shortcuts import get_object_or_404, redirect from django.contrib import messages @@ -85,9 +85,15 @@ class AttendanceListView(LoginRequiredMixin, ListView): context_object_name = 'attendances' 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): + model = Attendance - fields = ['employee', 'date', 'check_in', 'check_out', 'device', 'notes'] + form_class = AttendanceForm template_name = 'hr/attendance_form.html' success_url = reverse_lazy('hr:attendance_list') @@ -98,7 +104,7 @@ class AttendanceCreateView(LoginRequiredMixin, CreateView): class AttendanceUpdateView(LoginRequiredMixin, UpdateView): model = Attendance - fields = ['employee', 'date', 'check_in', 'check_out', 'device', 'notes'] + form_class = AttendanceForm template_name = 'hr/attendance_form.html' success_url = reverse_lazy('hr:attendance_list') @@ -108,6 +114,7 @@ class AttendanceUpdateView(LoginRequiredMixin, UpdateView): return context class LeaveRequestListView(LoginRequiredMixin, ListView): + model = LeaveRequest template_name = 'hr/leave_list.html' context_object_name = 'leaves' @@ -186,4 +193,30 @@ def sync_device_logs(request, pk): else: messages.success(request, _("Sync Successful! Fetched %(total)s records, %(new)s new.") % {'total': result['total'], 'new': result['new']}) - return redirect('hr:device_list') \ No newline at end of file + 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') \ No newline at end of file
{% trans "Date" %}{% trans "Date" %} {% trans "Employee" %} {% trans "Check In" %} {% trans "Check Out" %}{% trans "Actions" %}{% trans "Device" %}{% trans "Actions" %}
{{ att.date }}{{ att.employee }}{{ att.check_in|default:"--" }}{{ att.check_out|default:"--" }} - - + + {{ att.date|date:"d M Y" }} + +
+
+ +
+ {{ att.employee }} +
+
+ {% if att.check_in %} + + {{ att.check_in|time:"H:i" }} + + {% else %} + -- + {% endif %} + + {% if att.check_out %} + + {{ att.check_out|time:"H:i" }} + + {% else %} + -- + {% endif %} + + + {% if att.device %} + {{ att.device.name }} + {% else %} + {% trans "Manual" %} + {% endif %} + + + +
{% trans "No attendance records found." %} +
+ +

{% trans "No attendance records found." %}

+ + {% trans "Add First Record" %} + +
+