306 lines
12 KiB
Python
306 lines
12 KiB
Python
import os
|
|
import platform
|
|
import json
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.utils import timezone
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db.models import Sum, Q
|
|
from django.core.mail import send_mail
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.http import JsonResponse
|
|
from .models import Worker, Project, Team, WorkLog, PayrollRecord
|
|
from .forms import WorkLogForm
|
|
from datetime import timedelta
|
|
|
|
def home(request):
|
|
"""Render the landing screen with dashboard stats."""
|
|
workers_count = Worker.objects.count()
|
|
projects_count = Project.objects.count()
|
|
teams_count = Team.objects.count()
|
|
recent_logs = WorkLog.objects.order_by('-date')[:5]
|
|
|
|
# Analytics
|
|
# 1. Outstanding Payments
|
|
outstanding_total = 0
|
|
active_workers = Worker.objects.filter(is_active=True)
|
|
for worker in active_workers:
|
|
# Find unpaid logs for this worker
|
|
unpaid_logs_count = worker.work_logs.exclude(paid_in__worker=worker).count()
|
|
outstanding_total += unpaid_logs_count * worker.day_rate
|
|
|
|
# 2. Project Costs (Active Projects)
|
|
# Calculate sum of day_rates for all workers in all logs for each project
|
|
project_costs = []
|
|
active_projects = Project.objects.filter(is_active=True)
|
|
|
|
# Simple iteration for calculation (safer than complex annotations given properties)
|
|
for project in active_projects:
|
|
cost = 0
|
|
logs = project.logs.all()
|
|
for log in logs:
|
|
# We need to sum the day_rate of all workers in this log
|
|
# Optimization: prefetch workers if slow, but for now just iterate
|
|
for worker in log.workers.all():
|
|
cost += worker.day_rate
|
|
if cost > 0:
|
|
project_costs.append({'name': project.name, 'cost': cost})
|
|
|
|
# 3. Previous 2 months payments
|
|
two_months_ago = timezone.now().date() - timedelta(days=60)
|
|
recent_payments_total = PayrollRecord.objects.filter(date__gte=two_months_ago).aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
context = {
|
|
"workers_count": workers_count,
|
|
"projects_count": projects_count,
|
|
"teams_count": teams_count,
|
|
"recent_logs": recent_logs,
|
|
"current_time": timezone.now(),
|
|
"outstanding_total": outstanding_total,
|
|
"project_costs": project_costs,
|
|
"recent_payments_total": recent_payments_total,
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
def log_attendance(request):
|
|
# Build team workers map for frontend JS (needed for both GET and POST if re-rendering)
|
|
teams_qs = Team.objects.filter(is_active=True)
|
|
if request.user.is_authenticated and not request.user.is_superuser:
|
|
teams_qs = teams_qs.filter(supervisor=request.user)
|
|
|
|
team_workers_map = {}
|
|
for team in teams_qs:
|
|
# Get active workers for the team
|
|
active_workers = team.workers.filter(is_active=True).values_list('id', flat=True)
|
|
team_workers_map[team.id] = list(active_workers)
|
|
|
|
if request.method == 'POST':
|
|
form = WorkLogForm(request.POST, user=request.user)
|
|
if form.is_valid():
|
|
date = form.cleaned_data['date']
|
|
selected_workers = form.cleaned_data['workers']
|
|
conflict_action = request.POST.get('conflict_action')
|
|
|
|
# Check for existing logs for these workers on this date
|
|
# We want to find workers who ARE in selected_workers AND have a WorkLog on 'date'
|
|
conflicting_workers = Worker.objects.filter(
|
|
work_logs__date=date,
|
|
id__in=selected_workers.values_list('id', flat=True)
|
|
).distinct()
|
|
|
|
if conflicting_workers.exists() and not conflict_action:
|
|
context = {
|
|
'form': form,
|
|
'team_workers_json': json.dumps(team_workers_map),
|
|
'conflicting_workers': conflicting_workers,
|
|
'is_conflict': True,
|
|
'conflict_date': date,
|
|
}
|
|
return render(request, 'core/log_attendance.html', context)
|
|
|
|
# If we are here, either no conflicts or action is chosen
|
|
workers_to_save = list(selected_workers)
|
|
|
|
if conflict_action == 'skip':
|
|
# Exclude conflicting workers
|
|
conflicting_ids = conflicting_workers.values_list('id', flat=True)
|
|
workers_to_save = [w for w in selected_workers if w.id not in conflicting_ids]
|
|
|
|
if not workers_to_save:
|
|
messages.warning(request, "No new workers to log (all skipped).")
|
|
return redirect('home')
|
|
|
|
messages.success(request, f"Logged {len(workers_to_save)} workers (skipped {conflicting_workers.count()} duplicates).")
|
|
|
|
elif conflict_action == 'overwrite':
|
|
# Remove conflicting workers from their OLD logs
|
|
for worker in conflicting_workers:
|
|
old_logs = WorkLog.objects.filter(date=date, workers=worker)
|
|
for log in old_logs:
|
|
log.workers.remove(worker)
|
|
# Cleanup empty logs
|
|
if log.workers.count() == 0:
|
|
log.delete()
|
|
messages.success(request, f"Logged {len(workers_to_save)} workers (overwrote {conflicting_workers.count()} previous entries).")
|
|
|
|
else:
|
|
# No conflicts initially
|
|
messages.success(request, "Work log saved successfully.")
|
|
|
|
# Save the new log
|
|
work_log = form.save(commit=False)
|
|
if request.user.is_authenticated:
|
|
work_log.supervisor = request.user
|
|
work_log.save()
|
|
|
|
# Manually set workers
|
|
work_log.workers.set(workers_to_save)
|
|
|
|
return redirect('home')
|
|
else:
|
|
form = WorkLogForm(user=request.user if request.user.is_authenticated else None)
|
|
|
|
context = {
|
|
'form': form,
|
|
'team_workers_json': json.dumps(team_workers_map)
|
|
}
|
|
|
|
return render(request, 'core/log_attendance.html', context)
|
|
|
|
def work_log_list(request):
|
|
logs = WorkLog.objects.all().order_by('-date')
|
|
return render(request, 'core/work_log_list.html', {'logs': logs})
|
|
|
|
def manage_resources(request):
|
|
"""View to manage active status of resources."""
|
|
workers = Worker.objects.all().order_by('name')
|
|
projects = Project.objects.all().order_by('name')
|
|
teams = Team.objects.all().order_by('name')
|
|
|
|
context = {
|
|
'workers': workers,
|
|
'projects': projects,
|
|
'teams': teams,
|
|
}
|
|
return render(request, 'core/manage_resources.html', context)
|
|
|
|
def toggle_resource_status(request, model_type, pk):
|
|
"""Toggle the is_active status of a resource."""
|
|
if request.method == 'POST':
|
|
model_map = {
|
|
'worker': Worker,
|
|
'project': Project,
|
|
'team': Team,
|
|
}
|
|
|
|
model_class = model_map.get(model_type)
|
|
if model_class:
|
|
obj = get_object_or_404(model_class, pk=pk)
|
|
obj.is_active = not obj.is_active
|
|
obj.save()
|
|
|
|
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
|
|
return JsonResponse({
|
|
'success': True,
|
|
'is_active': obj.is_active,
|
|
'message': f"{obj.name} is now {'Active' if obj.is_active else 'Inactive'}."
|
|
})
|
|
|
|
return redirect('manage_resources')
|
|
|
|
def payroll_dashboard(request):
|
|
"""Dashboard for payroll management with filtering."""
|
|
status_filter = request.GET.get('status', 'pending') # pending, paid, all
|
|
|
|
# Common Analytics
|
|
outstanding_total = 0
|
|
active_workers = Worker.objects.filter(is_active=True).order_by('name')
|
|
|
|
workers_data = [] # For pending payments
|
|
|
|
for worker in active_workers:
|
|
unpaid_logs = worker.work_logs.exclude(paid_in__worker=worker)
|
|
count = unpaid_logs.count()
|
|
amount = count * worker.day_rate
|
|
|
|
if count > 0:
|
|
outstanding_total += amount
|
|
if status_filter in ['pending', 'all']:
|
|
workers_data.append({
|
|
'worker': worker,
|
|
'unpaid_count': count,
|
|
'unpaid_amount': amount,
|
|
'logs': unpaid_logs
|
|
})
|
|
|
|
# Paid History
|
|
paid_records = []
|
|
if status_filter in ['paid', 'all']:
|
|
paid_records = PayrollRecord.objects.select_related('worker').order_by('-date', '-id')
|
|
|
|
# Analytics: Project Costs (Active Projects)
|
|
project_costs = []
|
|
active_projects = Project.objects.filter(is_active=True)
|
|
|
|
for project in active_projects:
|
|
cost = 0
|
|
logs = project.logs.all()
|
|
for log in logs:
|
|
for worker in log.workers.all():
|
|
cost += worker.day_rate
|
|
if cost > 0:
|
|
project_costs.append({'name': project.name, 'cost': cost})
|
|
|
|
# Analytics: Previous 2 months payments
|
|
two_months_ago = timezone.now().date() - timedelta(days=60)
|
|
recent_payments_total = PayrollRecord.objects.filter(date__gte=two_months_ago).aggregate(total=Sum('amount'))['total'] or 0
|
|
|
|
context = {
|
|
'workers_data': workers_data,
|
|
'paid_records': paid_records,
|
|
'outstanding_total': outstanding_total,
|
|
'project_costs': project_costs,
|
|
'recent_payments_total': recent_payments_total,
|
|
'active_tab': status_filter,
|
|
}
|
|
return render(request, 'core/payroll_dashboard.html', context)
|
|
|
|
def process_payment(request, worker_id):
|
|
"""Process payment for a worker, mark logs as paid, and email receipt."""
|
|
worker = get_object_or_404(Worker, pk=worker_id)
|
|
|
|
if request.method == 'POST':
|
|
# Find unpaid logs
|
|
unpaid_logs = worker.work_logs.exclude(paid_in__worker=worker)
|
|
count = unpaid_logs.count()
|
|
|
|
if count > 0:
|
|
amount = count * worker.day_rate
|
|
|
|
# Create Payroll Record
|
|
payroll_record = PayrollRecord.objects.create(
|
|
worker=worker,
|
|
amount=amount,
|
|
date=timezone.now().date()
|
|
)
|
|
|
|
# Link logs
|
|
payroll_record.work_logs.set(unpaid_logs)
|
|
payroll_record.save()
|
|
|
|
# Email Notification
|
|
subject = f"Payslip for {worker.name} - {payroll_record.date}"
|
|
message = (
|
|
f"Payslip Generated\n\n"
|
|
f"Record ID: #{payroll_record.id}\n"
|
|
f"Worker: {worker.name}\n"
|
|
f"ID Number: {worker.id_no}\n"
|
|
f"Date: {payroll_record.date}\n"
|
|
f"Amount Paid: R {payroll_record.amount}\n\n"
|
|
f"This is an automated notification from Fox Fitt Payroll."
|
|
)
|
|
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
|
|
|
|
try:
|
|
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list)
|
|
messages.success(request, f"Payment of R {payroll_record.amount} processed for {worker.name}. Email sent to accounting.")
|
|
except Exception as e:
|
|
messages.warning(request, f"Payment processed, but email delivery failed: {str(e)}")
|
|
|
|
return redirect('payroll_dashboard')
|
|
|
|
return redirect('payroll_dashboard')
|
|
|
|
def payslip_detail(request, pk):
|
|
"""Show details of a payslip (Payment Record)."""
|
|
record = get_object_or_404(PayrollRecord, pk=pk)
|
|
|
|
# Get the logs included in this payment
|
|
logs = record.work_logs.all().order_by('date')
|
|
|
|
context = {
|
|
'record': record,
|
|
'logs': logs,
|
|
}
|
|
return render(request, 'core/payslip.html', context)
|