Files
LT_QR_Codes_Management/routes/attendance_edit.py
T
2026-09-11 22:42:45 -04:00

349 lines
16 KiB
Python

"""
routes/attendance_edit.py
=========================
Attendance record edit, manual entry, and delete routes.
Routes: /attendance/<id>/edit, /attendance/add,
/attendance/save_manual, /attendance/<id>/delete
"""
from flask import abort, Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, url_for
from datetime import datetime, date, timedelta, time
import io, os, json, re, traceback
from extensions import db, logger_handler
from models.attendance import AttendanceData
from models.employee import Employee
from models.permissions import UserLocationPermission, UserProjectPermission
from models.project import Project
from models.qrcode import QRCode
from models.user import User
from sqlalchemy import text, or_, and_
from logger_handler import log_user_activity, log_database_operations
from utils.helpers import (
admin_required,
get_client_ip,
has_admin_privileges,
has_staff_level_access,
login_required,
staff_or_admin_required)
from utils.geocoding import (calculate_location_accuracy_enhanced, process_location_data_enhanced,
check_location_accuracy_column_exists)
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from routes.attendance import bp # shared blueprint — do not redefine
@bp.route('/attendance/<int:record_id>/edit', methods=['GET', 'POST'], endpoint='edit_attendance')
@login_required
@log_database_operations('attendance_update')
def edit_attendance(record_id):
"""Edit attendance record (Admin and Payroll only)"""
# Check if user has permission to edit attendance records
if session.get('role') not in ['admin', 'payroll', 'accounting']:
flash('Access denied. Only administrators and accounting staff can edit attendance records.', 'error')
return redirect(url_for('attendance.attendance_report'))
try:
attendance_record = db.session.get(AttendanceData, record_id)
if attendance_record is None:
abort(404)
if request.method == 'POST':
# Get the audit note from form - REQUIRED
edit_note = request.form.get('edit_note', '').strip()
if not edit_note:
flash('Edit reason is required for audit purposes.', 'error')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
return render_template('edit_attendance.html',
attendance_record=attendance_record,
projects=projects,
qr_codes=QRCode.query.filter_by(active_status=True).all())
# Track changes for logging
changes = {}
old_values = {
'employee_id': attendance_record.employee_id,
'check_in_date': attendance_record.check_in_date,
'check_in_time': attendance_record.check_in_time,
'location_name': attendance_record.location_name,
'qr_code_id': attendance_record.qr_code_id,
'location_event': attendance_record.qr_code.location_event if attendance_record.qr_code else None
}
# Update attendance record fields
new_employee_id = request.form['employee_id'].strip().upper()
new_check_in_date = datetime.strptime(request.form['check_in_date'], '%Y-%m-%d').date()
new_check_in_time = datetime.strptime(request.form['check_in_time'], '%H:%M').time()
new_location_name = request.form['location_name'].strip()
# Get the new QR code ID from the form (this determines the location event)
new_qr_code_id = request.form.get('qr_code_id', '').strip()
if not new_qr_code_id:
flash('Location event selection is required.', 'error')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
return render_template('edit_attendance.html',
attendance_record=attendance_record,
projects=projects,
qr_codes=QRCode.query.filter_by(active_status=True).all())
# Validate the QR code exists
new_qr_code = db.session.get(QRCode, int(new_qr_code_id))
if not new_qr_code:
flash('Selected location event not found.', 'error')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
return render_template('edit_attendance.html',
attendance_record=attendance_record,
projects=projects,
qr_codes=QRCode.query.filter_by(active_status=True).all())
# Track what changed
if attendance_record.employee_id != new_employee_id:
changes['employee_id'] = f"{attendance_record.employee_id}{new_employee_id}"
if attendance_record.check_in_date != new_check_in_date:
changes['check_in_date'] = f"{attendance_record.check_in_date}{new_check_in_date}"
if attendance_record.check_in_time != new_check_in_time:
changes['check_in_time'] = f"{attendance_record.check_in_time}{new_check_in_time}"
if attendance_record.location_name != new_location_name:
changes['location_name'] = f"{attendance_record.location_name}{new_location_name}"
if attendance_record.qr_code_id != int(new_qr_code_id):
old_event = attendance_record.qr_code.location_event if attendance_record.qr_code else 'Unknown'
new_event = new_qr_code.location_event
changes['location_event'] = f"{old_event}{new_event}"
changes['qr_code_id'] = f"{attendance_record.qr_code_id}{new_qr_code_id}"
# Apply changes
attendance_record.employee_id = new_employee_id
attendance_record.check_in_date = new_check_in_date
attendance_record.check_in_time = new_check_in_time
attendance_record.location_name = new_location_name
attendance_record.qr_code_id = int(new_qr_code_id)
attendance_record.updated_timestamp = datetime.utcnow()
# Store the audit note with timestamp and user info
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
username = session.get('username', 'Unknown')
role = session.get('role', 'unknown')
new_note_entry = f"[{timestamp}] {role.title()} '{username}': {edit_note}"
if attendance_record.edit_note:
# Append to existing notes
attendance_record.edit_note = f"{attendance_record.edit_note}\n\n{new_note_entry}"
else:
# First edit note
attendance_record.edit_note = new_note_entry
db.session.commit()
# Enhanced logging with audit note
if changes:
logger_handler.log_security_event(
event_type="attendance_record_update",
description=f"{session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}",
severity="MEDIUM",
additional_data={
'record_id': record_id,
'changes': changes,
'user_role': session.get('role'),
'edit_reason': edit_note,
'editor_username': session.get('username')
}
)
logger_handler.logger.info(
f"User {session.get('username')} ({session.get('role', 'unknown')}) "
f"updated attendance record {record_id}: {changes}, reason: {edit_note}"
)
else:
# Log even if no changes were made (for audit purposes)
logger_handler.log_security_event(
event_type="attendance_record_edit_no_changes",
description=f"{session.get('role', 'unknown').title()} {session.get('username')} accessed edit form for record {record_id} but made no changes",
severity="LOW",
additional_data={
'record_id': record_id,
'user_role': session.get('role'),
'edit_reason': edit_note,
'editor_username': session.get('username')
}
)
logger_handler.logger.info(
f"User {session.get('username')} ({session.get('role', 'unknown')}) "
f"edited attendance record {record_id} with no changes, reason: {edit_note}"
)
flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success')
return redirect(url_for('attendance.attendance_report'))
# GET request - show edit form
# Get available projects for the dropdown
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
# Get available QR codes for location dropdown (for backward compatibility)
qr_codes = QRCode.query.filter_by(active_status=True).all()
return render_template('edit_attendance.html',
attendance_record=attendance_record,
projects=projects,
qr_codes=qr_codes)
except Exception as e:
db.session.rollback()
logger_handler.log_database_error('attendance_update', e)
logger_handler.logger.error(f"Error updating attendance record {record_id}: {e}", exc_info=True)
flash('Error updating attendance record. Please try again.', 'error')
return redirect(url_for('attendance.attendance_report'))
@bp.route('/attendance/add', methods=['GET'], endpoint='add_manual_attendance')
@login_required
@log_user_activity('manual_attendance_access')
def add_manual_attendance():
"""
Display form to manually add attendance record
Only accessible by admin and accounting roles
"""
try:
user_role = session.get('role')
# Check authorization
if user_role not in ['admin', 'accounting']:
flash('You do not have permission to manually add attendance records.', 'error')
return redirect(url_for('attendance.attendance_report'))
# Get all active projects
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
# Get today's date for form
today_date = datetime.now().strftime('%Y-%m-%d')
logger_handler.logger.info(
f"User {session.get('username')} ({user_role}) accessed manual attendance entry form"
)
return render_template('add_manual_attendance.html',
projects=projects,
today_date=today_date)
except Exception as e:
logger_handler.logger.error(f"Error loading manual attendance form: {e}")
flash('Error loading form. Please try again.', 'error')
return redirect(url_for('attendance.attendance_report'))
@bp.route('/attendance/save_manual', methods=['POST'], endpoint='save_manual_attendance')
@login_required
@log_user_activity('manual_attendance_creation')
@log_database_operations('manual_attendance_insert')
def save_manual_attendance():
"""
Save manually created attendance record
Only accessible by admin and accounting roles
"""
try:
user_role = session.get('role')
# Check authorization
if user_role not in ['admin', 'accounting']:
return jsonify({
'success': False,
'message': 'You do not have permission to manually add attendance records.'
}), 403
# Get form data
employee_id = request.form.get('employee_id', '').strip()
location_id = request.form.get('location_id', '').strip()
check_date = request.form.get('check_date', '').strip()
check_time = request.form.get('check_time', '').strip()
# Validate required fields
if not all([employee_id, location_id, check_date, check_time]):
flash('All fields are required.', 'error')
return redirect(url_for('attendance.add_manual_attendance'))
# Validate employee exists
employee = Employee.query.filter_by(id=int(employee_id)).first()
if not employee:
flash(f'Employee with ID {employee_id} not found.', 'error')
return redirect(url_for('attendance.add_manual_attendance'))
# Get QR code (location)
qr_code = db.session.get(QRCode, int(location_id))
if not qr_code:
flash('Selected location not found.', 'error')
return redirect(url_for('attendance.add_manual_attendance'))
# Parse date and time
try:
check_date_obj = datetime.strptime(check_date, '%Y-%m-%d').date()
check_time_obj = datetime.strptime(check_time, '%H:%M').time()
except ValueError as e:
flash('Invalid date or time format.', 'error')
logger_handler.logger.error(f"Date/time parsing error: {e}")
return redirect(url_for('attendance.add_manual_attendance'))
# Check if record already exists for this employee, location, date, and time
existing_record = AttendanceData.query.filter_by(
employee_id=str(employee_id),
qr_code_id=qr_code.id,
check_in_date=check_date_obj,
check_in_time=check_time_obj
).first()
if existing_record:
flash('An attendance record already exists for this employee at this location, date, and time.', 'warning')
return redirect(url_for('attendance.add_manual_attendance'))
# Create new attendance record
# Use QR code's location address for both QR address and check-in address
# Set fixed distance of 0.010 miles
new_attendance = AttendanceData(
qr_code_id=qr_code.id,
employee_id=str(employee_id),
check_in_date=check_date_obj,
check_in_time=check_time_obj,
location_name=qr_code.location,
# Use QR code's coordinates
latitude=qr_code.address_latitude,
longitude=qr_code.address_longitude,
# Use QR code's address for both
address=qr_code.location_address,
# Set fixed distance
location_accuracy=0.010,
accuracy=0.010,
# Mark as manual entry
location_source='manual_entry',
device_info='Manual Entry by Admin/Accounting',
user_agent=f'Manual Entry - User: {session.get("username")}',
ip_address=get_client_ip(),
status='present',
verification_required=False,
verification_status='approved',
created_timestamp=datetime.utcnow(),
updated_timestamp=datetime.utcnow()
)
db.session.add(new_attendance)
db.session.commit()
# Log the manual entry
logger_handler.logger.info(
f"Manual attendance record created by {session.get('username')} ({user_role}): "
f"Employee {employee.firstName} {employee.lastName} (ID: {employee_id}), "
f"Location: {qr_code.location}, Event: {qr_code.location_event}, "
f"Date: {check_date}, Time: {check_time}"
)
flash(f'Attendance record successfully created for {employee.firstName} {employee.lastName}.', 'success')
return redirect(url_for('attendance.attendance_report'))
except Exception as e:
db.session.rollback()
logger_handler.logger.error(f"Error saving manual attendance record: {e}")
logger_handler.logger.error(f"Error saving manual attendance record: {e}", exc_info=True)
flash('Error saving attendance record. Please try again.', 'error')
return redirect(url_for('attendance.add_manual_attendance'))