debugging expense report

This commit is contained in:
Flatlogic Bot 2026-02-12 15:53:09 +00:00
parent fa0a735548
commit c8c0620ceb
12 changed files with 320 additions and 63 deletions

View File

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

View File

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

View File

@ -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...'}),
}

View File

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

View File

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

View File

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

View File

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