38156-vm/core/views.py
Flatlogic Bot 091645a299 ver 7.1
2026-02-03 21:54:23 +00:00

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)