566 lines
21 KiB
Python
566 lines
21 KiB
Python
"""
|
|
Simple Acknowledgement Views
|
|
Simplified views for employee acknowledgement system.
|
|
"""
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils import timezone
|
|
from django.http import HttpResponse
|
|
from django.db.models import Count, Q
|
|
from django.core.paginator import Paginator
|
|
|
|
from apps.accounts.models import User, SimpleAcknowledgement, EmployeeAcknowledgement
|
|
|
|
|
|
# ============================================================================
|
|
# EMPLOYEE VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def simple_acknowledgement_list(request):
|
|
"""
|
|
Employee view: List all acknowledgements with sign status.
|
|
Shows acknowledgements that were sent to the employee OR are active and required.
|
|
"""
|
|
employee = request.user
|
|
|
|
# Get acknowledgements that were specifically sent to this employee
|
|
sent_ack_ids = EmployeeAcknowledgement.objects.filter(
|
|
employee=employee
|
|
).values_list('acknowledgement_id', flat=True)
|
|
|
|
# Get all active required acknowledgements that haven't been sent yet
|
|
active_required_ids = SimpleAcknowledgement.objects.filter(
|
|
is_active=True,
|
|
is_required=True
|
|
).exclude(
|
|
id__in=sent_ack_ids
|
|
).values_list('id', flat=True)
|
|
|
|
# Combine all acknowledgement IDs the employee needs to see
|
|
all_ack_ids = list(sent_ack_ids) + list(active_required_ids)
|
|
|
|
# Get all acknowledgements
|
|
acknowledgements = SimpleAcknowledgement.objects.filter(
|
|
id__in=all_ack_ids
|
|
).order_by('order', 'title')
|
|
|
|
# Get or create employee acknowledgements for each
|
|
employee_acks = []
|
|
for ack in acknowledgements:
|
|
emp_ack, created = EmployeeAcknowledgement.objects.get_or_create(
|
|
employee=employee,
|
|
acknowledgement=ack,
|
|
defaults={'is_signed': False}
|
|
)
|
|
employee_acks.append({
|
|
'acknowledgement': ack,
|
|
'signature': emp_ack,
|
|
'is_signed': emp_ack.is_signed,
|
|
'signed_at': emp_ack.signed_at,
|
|
'has_pdf': bool(emp_ack.signed_pdf),
|
|
'sent_at': emp_ack.sent_at,
|
|
'sent_by': emp_ack.sent_by,
|
|
})
|
|
|
|
# Separate pending and signed
|
|
pending_acks = [e for e in employee_acks if not e['is_signed']]
|
|
signed_acks = [e for e in employee_acks if e['is_signed']]
|
|
|
|
# Calculate stats
|
|
total = len(employee_acks)
|
|
signed = len(signed_acks)
|
|
pending = len(pending_acks)
|
|
|
|
context = {
|
|
'employee_acks': employee_acks,
|
|
'pending_acks': pending_acks,
|
|
'signed_acks': signed_acks,
|
|
'total': total,
|
|
'signed': signed,
|
|
'pending': pending,
|
|
'progress': int((signed / total * 100)) if total > 0 else 0,
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/list.html', context)
|
|
|
|
|
|
@login_required
|
|
def simple_acknowledgement_sign(request, ack_id):
|
|
"""
|
|
Employee view: Sign an acknowledgement.
|
|
Shows the acknowledgement details and a form to sign with name and employee ID.
|
|
"""
|
|
acknowledgement = get_object_or_404(SimpleAcknowledgement, id=ack_id, is_active=True)
|
|
employee = request.user
|
|
|
|
# Get or create the employee acknowledgement record
|
|
emp_ack, created = EmployeeAcknowledgement.objects.get_or_create(
|
|
employee=employee,
|
|
acknowledgement=acknowledgement,
|
|
defaults={'is_signed': False}
|
|
)
|
|
|
|
# If already signed, redirect to list
|
|
if emp_ack.is_signed:
|
|
messages.info(request, _('You have already signed this acknowledgement.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
if request.method == 'POST':
|
|
signature_name = request.POST.get('signature_name', '').strip()
|
|
signature_employee_id = request.POST.get('signature_employee_id', '').strip()
|
|
confirm = request.POST.get('confirm') == 'on'
|
|
|
|
# Validation
|
|
if not signature_name:
|
|
messages.error(request, _('Please enter your name.'))
|
|
elif not signature_employee_id:
|
|
messages.error(request, _('Please enter your employee ID.'))
|
|
elif not confirm:
|
|
messages.error(request, _('Please confirm that you have read and understood the acknowledgement.'))
|
|
else:
|
|
# Save the signature
|
|
emp_ack.is_signed = True
|
|
emp_ack.signed_at = timezone.now()
|
|
emp_ack.signature_name = signature_name
|
|
emp_ack.signature_employee_id = signature_employee_id
|
|
emp_ack.ip_address = get_client_ip(request)
|
|
emp_ack.user_agent = request.META.get('HTTP_USER_AGENT', '')[:500]
|
|
emp_ack.save()
|
|
|
|
# Generate simple signed PDF (optional - can be added later)
|
|
# For now, just copy the original PDF if it exists
|
|
if acknowledgement.pdf_document and not emp_ack.signed_pdf:
|
|
emp_ack.signed_pdf = acknowledgement.pdf_document
|
|
emp_ack.save()
|
|
|
|
messages.success(request, _('Acknowledgement signed successfully!'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
context = {
|
|
'acknowledgement': acknowledgement,
|
|
'emp_ack': emp_ack,
|
|
'employee': employee,
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/sign.html', context)
|
|
|
|
|
|
@login_required
|
|
def simple_acknowledgement_download(request, ack_id):
|
|
"""
|
|
Employee view: Download the signed PDF.
|
|
"""
|
|
emp_ack = get_object_or_404(
|
|
EmployeeAcknowledgement,
|
|
acknowledgement_id=ack_id,
|
|
employee=request.user,
|
|
is_signed=True
|
|
)
|
|
|
|
if emp_ack.signed_pdf:
|
|
response = HttpResponse(emp_ack.signed_pdf, content_type='application/pdf')
|
|
response['Content-Disposition'] = f'attachment; filename="{emp_ack.acknowledgement.title}.pdf"'
|
|
return response
|
|
elif emp_ack.acknowledgement.pdf_document:
|
|
response = HttpResponse(emp_ack.acknowledgement.pdf_document, content_type='application/pdf')
|
|
response['Content-Disposition'] = f'attachment; filename="{emp_ack.acknowledgement.title}.pdf"'
|
|
return response
|
|
else:
|
|
messages.error(request, _('No PDF available for this acknowledgement.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
|
|
# ============================================================================
|
|
# ADMIN VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def admin_acknowledgement_list(request):
|
|
"""
|
|
Admin view: List all acknowledgements with management options.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
acknowledgements = SimpleAcknowledgement.objects.all().order_by('order', 'title')
|
|
|
|
context = {
|
|
'acknowledgements': acknowledgements,
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/admin_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def admin_acknowledgement_create(request):
|
|
"""
|
|
Admin view: Create a new acknowledgement.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
if request.method == 'POST':
|
|
title = request.POST.get('title', '').strip()
|
|
description = request.POST.get('description', '').strip()
|
|
pdf_document = request.FILES.get('pdf_document')
|
|
is_required = request.POST.get('is_required') == 'on'
|
|
is_active = request.POST.get('is_active') == 'on'
|
|
order = request.POST.get('order', '0')
|
|
|
|
if not title:
|
|
messages.error(request, _('Please enter a title.'))
|
|
else:
|
|
acknowledgement = SimpleAcknowledgement.objects.create(
|
|
title=title,
|
|
description=description,
|
|
pdf_document=pdf_document,
|
|
is_required=is_required,
|
|
is_active=is_active,
|
|
order=int(order) if order.isdigit() else 0,
|
|
)
|
|
messages.success(request, _('Acknowledgement created successfully!'))
|
|
return redirect('accounts:simple_acknowledgements:admin_list')
|
|
|
|
return render(request, 'accounts/simple_acknowledgements/admin_create.html')
|
|
|
|
|
|
@login_required
|
|
def admin_acknowledgement_edit(request, ack_id):
|
|
"""
|
|
Admin view: Edit an existing acknowledgement.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
acknowledgement = get_object_or_404(SimpleAcknowledgement, id=ack_id)
|
|
|
|
if request.method == 'POST':
|
|
title = request.POST.get('title', '').strip()
|
|
description = request.POST.get('description', '').strip()
|
|
pdf_document = request.FILES.get('pdf_document')
|
|
is_required = request.POST.get('is_required') == 'on'
|
|
is_active = request.POST.get('is_active') == 'on'
|
|
order = request.POST.get('order', '0')
|
|
|
|
if not title:
|
|
messages.error(request, _('Please enter a title.'))
|
|
else:
|
|
acknowledgement.title = title
|
|
acknowledgement.description = description
|
|
acknowledgement.is_required = is_required
|
|
acknowledgement.is_active = is_active
|
|
acknowledgement.order = int(order) if order.isdigit() else 0
|
|
|
|
if pdf_document:
|
|
acknowledgement.pdf_document = pdf_document
|
|
|
|
acknowledgement.save()
|
|
messages.success(request, _('Acknowledgement updated successfully!'))
|
|
return redirect('accounts:simple_acknowledgements:admin_list')
|
|
|
|
context = {
|
|
'acknowledgement': acknowledgement,
|
|
'action': 'edit',
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/admin_form.html', context)
|
|
|
|
|
|
@login_required
|
|
def admin_acknowledgement_delete(request, ack_id):
|
|
"""
|
|
Admin view: Delete an acknowledgement.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
acknowledgement = get_object_or_404(SimpleAcknowledgement, id=ack_id)
|
|
|
|
if request.method == 'POST':
|
|
acknowledgement.delete()
|
|
messages.success(request, _('Acknowledgement deleted successfully!'))
|
|
return redirect('accounts:simple_acknowledgements:admin_list')
|
|
|
|
context = {
|
|
'acknowledgement': acknowledgement,
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/admin_delete.html', context)
|
|
|
|
|
|
@login_required
|
|
def admin_employee_signatures(request, ack_id=None):
|
|
"""
|
|
Admin view: View all employee signatures.
|
|
Can filter by acknowledgement if ack_id is provided.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
signatures = EmployeeAcknowledgement.objects.select_related(
|
|
'employee', 'acknowledgement'
|
|
).order_by('-signed_at')
|
|
|
|
if ack_id:
|
|
acknowledgement = get_object_or_404(SimpleAcknowledgement, id=ack_id)
|
|
signatures = signatures.filter(acknowledgement=acknowledgement)
|
|
else:
|
|
acknowledgement = None
|
|
|
|
# Search
|
|
search = request.GET.get('search', '')
|
|
if search:
|
|
signatures = signatures.filter(
|
|
Q(employee__email__icontains=search) |
|
|
Q(employee__first_name__icontains=search) |
|
|
Q(employee__last_name__icontains=search) |
|
|
Q(signature_name__icontains=search) |
|
|
Q(signature_employee_id__icontains=search)
|
|
)
|
|
|
|
# Filter by status
|
|
status_filter = request.GET.get('status', '')
|
|
if status_filter == 'signed':
|
|
signatures = signatures.filter(is_signed=True)
|
|
elif status_filter == 'pending':
|
|
signatures = signatures.filter(is_signed=False)
|
|
|
|
# Pagination
|
|
paginator = Paginator(signatures, 50)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'signatures': page_obj,
|
|
'acknowledgement': acknowledgement,
|
|
'search': search,
|
|
'status_filter': status_filter,
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/admin_signatures.html', context)
|
|
|
|
|
|
@login_required
|
|
def admin_export_signatures(request):
|
|
"""
|
|
Admin view: Export all signatures to CSV.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
import csv
|
|
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="acknowledgement_signatures.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Employee Email', 'Employee Name', 'Employee ID (at sign)',
|
|
'Acknowledgement', 'Status', 'Signed At', 'IP Address'
|
|
])
|
|
|
|
signatures = EmployeeAcknowledgement.objects.select_related(
|
|
'employee', 'acknowledgement'
|
|
).order_by('-signed_at')
|
|
|
|
for sig in signatures:
|
|
writer.writerow([
|
|
sig.employee.email,
|
|
sig.signature_name or sig.employee.get_full_name(),
|
|
sig.signature_employee_id,
|
|
sig.acknowledgement.title,
|
|
'Signed' if sig.is_signed else 'Pending',
|
|
sig.signed_at.strftime('%Y-%m-%d %H:%M') if sig.signed_at else '',
|
|
sig.ip_address or '',
|
|
])
|
|
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def admin_send_acknowledgement(request, ack_id):
|
|
"""
|
|
Admin view: Send acknowledgement to specific staff members.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
acknowledgement = get_object_or_404(SimpleAcknowledgement, id=ack_id)
|
|
|
|
# Get all active staff
|
|
from apps.accounts.models import User
|
|
staff_list = User.objects.filter(
|
|
is_active=True,
|
|
is_provisional=False
|
|
).exclude(
|
|
id=request.user.id
|
|
).order_by('first_name', 'last_name')
|
|
|
|
# Get already sent employees
|
|
already_sent_ids = EmployeeAcknowledgement.objects.filter(
|
|
acknowledgement=acknowledgement
|
|
).values_list('employee_id', flat=True)
|
|
|
|
if request.method == 'POST':
|
|
employee_ids = request.POST.getlist('employees')
|
|
send_email = request.POST.get('send_email') == 'on'
|
|
|
|
if not employee_ids:
|
|
messages.error(request, _('Please select at least one employee.'))
|
|
else:
|
|
sent_count = 0
|
|
for emp_id in employee_ids:
|
|
emp_ack, created = EmployeeAcknowledgement.objects.get_or_create(
|
|
employee_id=emp_id,
|
|
acknowledgement=acknowledgement,
|
|
defaults={
|
|
'sent_at': timezone.now(),
|
|
'sent_by': request.user,
|
|
}
|
|
)
|
|
if created:
|
|
sent_count += 1
|
|
# Update sent info if record already existed but wasn't sent
|
|
elif not emp_ack.sent_at:
|
|
emp_ack.sent_at = timezone.now()
|
|
emp_ack.sent_by = request.user
|
|
emp_ack.save()
|
|
sent_count += 1
|
|
|
|
# Send email notifications if requested
|
|
if send_email and sent_count > 0:
|
|
try:
|
|
from apps.notifications.services import NotificationService
|
|
|
|
employees = User.objects.filter(id__in=employee_ids)
|
|
for emp in employees:
|
|
NotificationService.send_email(
|
|
email=emp.email,
|
|
subject=f'Action Required: {acknowledgement.title}',
|
|
message=f'''Dear {emp.get_full_name() or emp.email},
|
|
|
|
You have been requested to sign the following acknowledgement: {acknowledgement.title}
|
|
|
|
Please log in to the system to review and sign this document.
|
|
|
|
Best regards,
|
|
HR Department''',
|
|
related_object=emp_ack,
|
|
)
|
|
except Exception as e:
|
|
messages.warning(request, _('Acknowledgements sent but email notification failed.'))
|
|
|
|
messages.success(request, _('Acknowledgement sent to {count} employee(s).').format(count=sent_count))
|
|
return redirect('accounts:simple_acknowledgements:admin_list')
|
|
|
|
context = {
|
|
'acknowledgement': acknowledgement,
|
|
'staff_list': staff_list,
|
|
'already_sent_ids': list(already_sent_ids),
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/admin_send.html', context)
|
|
|
|
|
|
@login_required
|
|
def admin_upload_signed_pdf(request, signature_id):
|
|
"""
|
|
Admin view: Upload signed PDF on behalf of an employee.
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
emp_ack = get_object_or_404(
|
|
EmployeeAcknowledgement.objects.select_related('employee', 'acknowledgement'),
|
|
id=signature_id
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
signed_pdf = request.FILES.get('signed_pdf')
|
|
signature_name = request.POST.get('signature_name', emp_ack.employee.get_full_name())
|
|
signature_employee_id = request.POST.get('signature_employee_id', '')
|
|
notes = request.POST.get('notes', '')
|
|
|
|
if not signed_pdf:
|
|
messages.error(request, _('Please upload a signed PDF.'))
|
|
else:
|
|
emp_ack.signed_pdf = signed_pdf
|
|
emp_ack.is_signed = True
|
|
emp_ack.signed_at = timezone.now()
|
|
emp_ack.signature_name = signature_name
|
|
emp_ack.signature_employee_id = signature_employee_id
|
|
emp_ack.notes = notes
|
|
emp_ack.save()
|
|
|
|
messages.success(
|
|
request,
|
|
_('Signed PDF uploaded successfully for {name}.').format(name=emp_ack.employee.get_full_name())
|
|
)
|
|
return redirect('accounts:simple_acknowledgements:admin_signatures')
|
|
|
|
context = {
|
|
'emp_ack': emp_ack,
|
|
'employee': emp_ack.employee,
|
|
'acknowledgement': emp_ack.acknowledgement,
|
|
}
|
|
return render(request, 'accounts/simple_acknowledgements/admin_upload_pdf.html', context)
|
|
|
|
|
|
@login_required
|
|
def admin_mark_as_sent(request, ack_id):
|
|
"""
|
|
Admin view: Mark acknowledgement as sent to all employees (bulk action).
|
|
"""
|
|
if not request.user.is_staff and not request.user.is_px_admin():
|
|
messages.error(request, _('You do not have permission to access this page.'))
|
|
return redirect('accounts:simple_acknowledgements:employee_list')
|
|
|
|
acknowledgement = get_object_or_404(SimpleAcknowledgement, id=ack_id)
|
|
|
|
if request.method == 'POST':
|
|
from apps.accounts.models import User
|
|
|
|
# Get all active employees who haven't been sent this yet
|
|
all_employees = User.objects.filter(is_active=True, is_provisional=False)
|
|
already_sent = EmployeeAcknowledgement.objects.filter(
|
|
acknowledgement=acknowledgement
|
|
).values_list('employee_id', flat=True)
|
|
|
|
new_employees = all_employees.exclude(id__in=already_sent)
|
|
|
|
created_count = 0
|
|
for emp in new_employees:
|
|
EmployeeAcknowledgement.objects.create(
|
|
employee=emp,
|
|
acknowledgement=acknowledgement,
|
|
sent_at=timezone.now(),
|
|
sent_by=request.user,
|
|
)
|
|
created_count += 1
|
|
|
|
messages.success(
|
|
request,
|
|
_('Acknowledgement sent to {count} new employee(s).').format(count=created_count)
|
|
)
|
|
return redirect('accounts:simple_acknowledgements:admin_list')
|
|
|
|
return redirect('accounts:simple_acknowledgements:admin_list')
|
|
|
|
|
|
# ============================================================================
|
|
# HELPER FUNCTIONS
|
|
# ============================================================================
|
|
|
|
def get_client_ip(request):
|
|
"""Get client IP address from request."""
|
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if x_forwarded_for:
|
|
ip = x_forwarded_for.split(',')[0]
|
|
else:
|
|
ip = request.META.get('REMOTE_ADDR')
|
|
return ip
|