import os import platform import json import csv 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, Prefetch from django.core.mail import send_mail from django.conf import settings from django.contrib import messages from django.http import JsonResponse, HttpResponse 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): """View work log history with advanced filtering.""" worker_id = request.GET.get('worker') team_id = request.GET.get('team') project_id = request.GET.get('project') payment_status = request.GET.get('payment_status') # 'paid', 'unpaid', 'all' logs = WorkLog.objects.all().prefetch_related('workers', 'project', 'supervisor', 'paid_in').order_by('-date', '-id') target_worker = None if worker_id: logs = logs.filter(workers__id=worker_id) # Fetch the worker to get the day rate reliably target_worker = Worker.objects.filter(id=worker_id).first() if team_id: # Find workers in this team and filter logs containing them team_workers = Worker.objects.filter(teams__id=team_id) logs = logs.filter(workers__in=team_workers).distinct() if project_id: logs = logs.filter(project_id=project_id) if payment_status == 'paid': # Logs that are linked to at least one PayrollRecord logs = logs.filter(paid_in__isnull=False).distinct() elif payment_status == 'unpaid': # This is tricky because a log can have multiple workers, some paid some not. # But usually a WorkLog is marked paid when its workers are paid. # If we filtered by worker, we can check if THAT worker is paid in that log. if worker_id: worker = get_object_or_404(Worker, pk=worker_id) logs = logs.exclude(paid_in__worker=worker) else: logs = logs.filter(paid_in__isnull=True) # Calculate amounts for display # Convert to list to attach attributes final_logs = [] total_amount = 0 for log in logs: if target_worker: log.display_amount = target_worker.day_rate else: # Sum of all workers in this log log.display_amount = sum(w.day_rate for w in log.workers.all()) final_logs.append(log) total_amount += log.display_amount # Context for filters context = { 'logs': final_logs, 'total_amount': total_amount, 'workers': Worker.objects.filter(is_active=True).order_by('name'), 'teams': Team.objects.filter(is_active=True).order_by('name'), 'projects': Project.objects.filter(is_active=True).order_by('name'), 'selected_worker': int(worker_id) if worker_id else None, 'selected_team': int(team_id) if team_id else None, 'selected_project': int(project_id) if project_id else None, 'selected_payment_status': payment_status, 'target_worker': target_worker, } return render(request, 'core/work_log_list.html', context) def export_work_log_csv(request): """Export filtered work logs to CSV.""" worker_id = request.GET.get('worker') team_id = request.GET.get('team') project_id = request.GET.get('project') payment_status = request.GET.get('payment_status') logs = WorkLog.objects.all().prefetch_related('workers', 'project', 'supervisor', 'paid_in').order_by('-date', '-id') target_worker = None if worker_id: logs = logs.filter(workers__id=worker_id) target_worker = Worker.objects.filter(id=worker_id).first() if team_id: team_workers = Worker.objects.filter(teams__id=team_id) logs = logs.filter(workers__in=team_workers).distinct() if project_id: logs = logs.filter(project_id=project_id) if payment_status == 'paid': logs = logs.filter(paid_in__isnull=False).distinct() elif payment_status == 'unpaid': if worker_id: worker = get_object_or_404(Worker, pk=worker_id) logs = logs.exclude(paid_in__worker=worker) else: logs = logs.filter(paid_in__isnull=True) response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="work_logs.csv"' writer = csv.writer(response) writer.writerow(['Date', 'Project', 'Workers', 'Amount', 'Payment Status', 'Supervisor']) for log in logs: # Amount Logic if target_worker: display_amount = target_worker.day_rate workers_str = target_worker.name else: display_amount = sum(w.day_rate for w in log.workers.all()) workers_str = ", ".join([w.name for w in log.workers.all()]) # Payment Status Logic is_paid = log.paid_in.exists() status_str = "Paid" if is_paid else "Pending" writer.writerow([ log.date, log.project.name, workers_str, f"{display_amount:.2f}", status_str, log.supervisor.username if log.supervisor else "System" ]) return response def manage_resources(request): """View to manage active status of resources.""" # Prefetch teams for workers to avoid N+1 in template workers = Worker.objects.all().prefetch_related('teams').order_by('name') projects = Project.objects.all().order_by('name') teams = Team.objects.all().prefetch_related('workers').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)