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 .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() 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)