04/29 Removed payroll functionality from the app

This commit is contained in:
2026-04-29 16:21:05 -04:00
parent 1069de5ce4
commit a4405e2aa0
6 changed files with 2 additions and 3378 deletions
+1 -2
View File
@@ -88,13 +88,12 @@ def create_app() -> Flask:
import routes.attendance_edit # noqa: F401 import routes.attendance_edit # noqa: F401
import routes.verification # noqa: F401 import routes.verification # noqa: F401
import routes.attendance_export # noqa: F401 import routes.attendance_export # noqa: F401
from routes.payroll import bp as payroll_bp
from routes.statistics import bp as statistics_bp from routes.statistics import bp as statistics_bp
from routes.employees import bp as employees_bp from routes.employees import bp as employees_bp
from routes.time_attendance import bp as time_attendance_bp from routes.time_attendance import bp as time_attendance_bp
for bp in (auth_bp, dashboard_bp, users_bp, admin_bp, projects_bp, for bp in (auth_bp, dashboard_bp, users_bp, admin_bp, projects_bp,
qr_codes_bp, attendance_bp, payroll_bp, statistics_bp, qr_codes_bp, attendance_bp, statistics_bp,
employees_bp, time_attendance_bp): employees_bp, time_attendance_bp):
app.register_blueprint(bp) app.register_blueprint(bp)
-391
View File
@@ -1,391 +0,0 @@
"""
Enhanced Payroll Excel Exporter with SP/PW Support
=================================================
Extends the existing PayrollExcelExporter to support Special Project (SP)
and Periodic Work (PW) hours in Excel reports.
"""
import io
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Any
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from working_hours_calculator import WorkingHoursCalculator
from logger_handler import log_database_operations
class EnhancedPayrollExcelExporter:
"""Enhanced Excel exporter with SP/PW support"""
def __init__(self, company_name: str = "Your Company Name"):
self.company_name = company_name
self._setup_styles()
def _setup_styles(self):
"""Setup Excel cell styles"""
# Header styles
self.header_style = {
'font': Font(name="Arial", size=11, bold=True, color="FFFFFF"),
'fill': PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid"),
'alignment': Alignment(horizontal="center", vertical="center"),
'border': Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin")
)
}
# Data styles
self.data_style = {
'font': Font(name="Arial", size=10),
'alignment': Alignment(horizontal="center", vertical="center"),
'border': Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin")
)
}
# Summary styles
self.summary_style = {
'font': Font(name="Arial", size=11, bold=True),
'fill': PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid"),
'alignment': Alignment(horizontal="center", vertical="center"),
'border': Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin")
)
}
def _apply_style(self, cell, style_dict):
"""Apply style dictionary to a cell"""
for attr, value in style_dict.items():
setattr(cell, attr, value)
@log_database_operations('enhanced_payroll_export')
def create_enhanced_payroll_report(self, start_date: datetime, end_date: datetime,
attendance_records: List[Dict], employee_names: Dict[str, str] = None,
project_name: str = None) -> io.BytesIO:
"""
Create enhanced payroll report with SP/PW hours
Args:
start_date: Report start date
end_date: Report end date
attendance_records: List of attendance records
employee_names: Dictionary mapping employee_id to full name
project_name: Project name for the report
Returns:
BytesIO buffer containing the Excel file
"""
try:
print(f"📊 Creating enhanced payroll report with SP/PW support from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
# Get employee names using existing method
if employee_names is None:
employee_names = self._get_employee_names(attendance_records)
# Calculate working hours using enhanced calculator
calculator = WorkingHoursCalculator()
working_hours_data = calculator.calculate_all_employees_hours(
start_date, end_date, attendance_records
)
# Create workbook
workbook = Workbook()
worksheet = workbook.active
worksheet.title = "Enhanced Payroll Report"
# Write report header
current_row = self._write_enhanced_header(worksheet, start_date, end_date, project_name)
# Write column headers
headers = [
"Employee ID", "Employee Name", "Total Hours", "Regular Hours",
"Overtime Hours", "SP Hours", "PW Hours", "Working Days", "Status"
]
for col, header in enumerate(headers, 1):
cell = worksheet.cell(row=current_row, column=col, value=header)
self._apply_style(cell, self.header_style)
current_row += 1
# Write employee data
total_employees = 0
totals = {
'total_hours': 0.0,
'regular_hours': 0.0,
'overtime_hours': 0.0,
'sp_hours': 0.0,
'pw_hours': 0.0
}
for employee_id, emp_data in working_hours_data['employees'].items():
employee_name = employee_names.get(employee_id, f'Employee {employee_id}')
# Calculate working days and status
working_days = len([d for d in emp_data['daily_hours'].values() if d['total_hours'] > 0])
miss_punches = len([d for d in emp_data['daily_hours'].values() if d['is_miss_punch']])
status = f"{miss_punches} miss punch(es)" if miss_punches > 0 else "Complete"
# Get grand totals
grand_totals = emp_data['grand_totals']
row_data = [
employee_id,
employee_name,
round(grand_totals['total_hours'], 2),
round(grand_totals['regular_hours'], 2),
round(grand_totals['overtime_hours'], 2),
round(grand_totals['sp_hours'], 2),
round(grand_totals['pw_hours'], 2),
working_days,
status
]
for col, value in enumerate(row_data, 1):
cell = worksheet.cell(row=current_row, column=col, value=value)
self._apply_style(cell, self.data_style)
# Highlight miss punches
if miss_punches > 0 and col == 9: # Status column
cell.fill = PatternFill(start_color="FFE6E6", end_color="FFE6E6", fill_type="solid")
# Add to totals
totals['total_hours'] += grand_totals['total_hours']
totals['regular_hours'] += grand_totals['regular_hours']
totals['overtime_hours'] += grand_totals['overtime_hours']
totals['sp_hours'] += grand_totals['sp_hours']
totals['pw_hours'] += grand_totals['pw_hours']
total_employees += 1
current_row += 1
# Write summary section
current_row = self._write_enhanced_summary(worksheet, current_row, total_employees, totals)
# Auto-adjust columns
self._auto_adjust_columns(worksheet)
# Save to BytesIO
excel_buffer = io.BytesIO()
workbook.save(excel_buffer)
excel_buffer.seek(0)
print("✅ Enhanced payroll report Excel file created successfully")
return excel_buffer
except Exception as e:
print(f"❌ Error creating enhanced payroll report: {e}")
raise e
def _write_enhanced_header(self, worksheet, start_date: datetime, end_date: datetime, project_name: str) -> int:
"""Write enhanced report header"""
current_row = 1
# Company name
cell = worksheet.cell(row=current_row, column=1, value=self.company_name)
cell.font = Font(name="Arial", size=14, bold=True)
worksheet.merge_cells(f'A{current_row}:I{current_row}')
current_row += 1
# Report title
cell = worksheet.cell(row=current_row, column=1, value="Enhanced Payroll Report with SP/PW Hours")
cell.font = Font(name="Arial", size=12, bold=True)
worksheet.merge_cells(f'A{current_row}:I{current_row}')
current_row += 1
# Project name
if project_name:
cell = worksheet.cell(row=current_row, column=1, value=f"Project: {project_name}")
cell.font = Font(name="Arial", size=11)
worksheet.merge_cells(f'A{current_row}:I{current_row}')
current_row += 1
# Date range
date_range = f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
cell = worksheet.cell(row=current_row, column=1, value=date_range)
cell.font = Font(name="Arial", size=11)
worksheet.merge_cells(f'A{current_row}:I{current_row}')
current_row += 2 # Add space
return current_row
def _write_enhanced_summary(self, worksheet, start_row: int, total_employees: int, totals: Dict) -> int:
"""Write enhanced summary section"""
current_row = start_row + 1
# Summary header
cell = worksheet.cell(row=current_row, column=1, value="SUMMARY")
cell.font = Font(name="Arial", size=12, bold=True)
worksheet.merge_cells(f'A{current_row}:I{current_row}')
current_row += 1
# Summary data
summary_data = [
("Total Employees:", total_employees),
("Total Hours:", round(totals['total_hours'], 2)),
("Regular Hours:", round(totals['regular_hours'], 2)),
("Overtime Hours:", round(totals['overtime_hours'], 2)),
("Special Project (SP) Hours:", round(totals['sp_hours'], 2)),
("Periodic Work (PW) Hours:", round(totals['pw_hours'], 2)),
("Report Generated:", datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
]
for label, value in summary_data:
cell1 = worksheet.cell(row=current_row, column=1, value=label)
self._apply_style(cell1, self.summary_style)
cell2 = worksheet.cell(row=current_row, column=2, value=value)
self._apply_style(cell2, self.summary_style)
current_row += 1
return current_row
def _get_employee_names(self, attendance_records: List[Dict]) -> Dict[str, str]:
"""Get employee names from attendance records"""
# This method should be implemented based on your existing database structure
# For now, return a basic implementation
employee_names = {}
for record in attendance_records:
if hasattr(record, '__dict__'):
emp_id = str(record.employee_id)
if emp_id not in employee_names:
employee_names[emp_id] = f'Employee {emp_id}'
else:
emp_id = str(record['employee_id'])
if emp_id not in employee_names:
employee_names[emp_id] = f'Employee {emp_id}'
return employee_names
def _auto_adjust_columns(self, worksheet):
"""Auto-adjust column widths"""
for column in worksheet.columns:
max_length = 0
column_letter = get_column_letter(column[0].column)
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 25)
worksheet.column_dimensions[column_letter].width = adjusted_width
@log_database_operations('detailed_sp_pw_export')
def create_detailed_sp_pw_report(self, start_date: datetime, end_date: datetime,
attendance_records: List[Dict], employee_names: Dict[str, str] = None) -> io.BytesIO:
"""
Create detailed daily report showing SP/PW breakdown by day
"""
try:
print(f"📊 Creating detailed SP/PW daily report")
# Get employee names
if employee_names is None:
employee_names = self._get_employee_names(attendance_records)
# Calculate working hours
calculator = WorkingHoursCalculator()
working_hours_data = calculator.calculate_all_employees_hours(
start_date, end_date, attendance_records
)
# Create workbook
workbook = Workbook()
worksheet = workbook.active
worksheet.title = "Detailed SP/PW Report"
# Write header
current_row = 1
cell = worksheet.cell(row=current_row, column=1, value="Detailed Daily Hours Report with SP/PW Breakdown")
cell.font = Font(name="Arial", size=14, bold=True)
worksheet.merge_cells(f'A{current_row}:K{current_row}')
current_row += 2
# Column headers
headers = [
"Employee ID", "Employee Name", "Date", "Day", "Regular Hours",
"SP Hours", "PW Hours", "Total Hours", "Records", "Status", "Notes"
]
for col, header in enumerate(headers, 1):
cell = worksheet.cell(row=current_row, column=col, value=header)
self._apply_style(cell, self.header_style)
current_row += 1
# Write daily data for each employee
for employee_id, emp_data in working_hours_data['employees'].items():
employee_name = employee_names.get(employee_id, f'Employee {employee_id}')
for date_str, day_data in emp_data['daily_hours'].items():
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
day_name = date_obj.strftime('%A')
# Get hours by type
regular_hours = day_data.get('regular_hours', 0.0)
sp_hours = day_data.get('sp_hours', 0.0)
pw_hours = day_data.get('pw_hours', 0.0)
total_hours = day_data.get('total_hours', 0.0)
# Status and notes
if day_data.get('is_miss_punch', False):
status = "Miss Punch"
notes = "Incomplete records"
elif total_hours == 0:
status = "No Work"
notes = "No attendance records"
else:
status = "Complete"
notes = f"{day_data['records_count']} record(s)"
row_data = [
employee_id,
employee_name,
date_str,
day_name,
round(regular_hours, 2),
round(sp_hours, 2),
round(pw_hours, 2),
round(total_hours, 2),
day_data['records_count'],
status,
notes
]
for col, value in enumerate(row_data, 1):
cell = worksheet.cell(row=current_row, column=col, value=value)
self._apply_style(cell, self.data_style)
# Color coding
if status == "Miss Punch":
cell.fill = PatternFill(start_color="FFE6E6", end_color="FFE6E6", fill_type="solid")
elif sp_hours > 0 and col == 6: # SP Hours column
cell.fill = PatternFill(start_color="E6F3FF", end_color="E6F3FF", fill_type="solid")
elif pw_hours > 0 and col == 7: # PW Hours column
cell.fill = PatternFill(start_color="FFF2E6", end_color="FFF2E6", fill_type="solid")
current_row += 1
# Auto-adjust columns
self._auto_adjust_columns(worksheet)
# Save to BytesIO
excel_buffer = io.BytesIO()
workbook.save(excel_buffer)
excel_buffer.seek(0)
print("✅ Detailed SP/PW report Excel file created successfully")
return excel_buffer
except Exception as e:
print(f"❌ Error creating detailed SP/PW report: {e}")
raise e
File diff suppressed because it is too large Load Diff
-655
View File
@@ -1,655 +0,0 @@
"""
routes/payroll.py
=================
Payroll dashboard and Excel export routes.
Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate,
/api/employee/<id>/miss-punch-details
"""
from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, url_for, current_app
from datetime import datetime, date, timedelta, time
import io, json, traceback, os
from extensions import db, logger_handler
from models.attendance import AttendanceData
from models.employee import Employee
from models.project import Project
from models.qrcode import QRCode
from models.user import User
from sqlalchemy import text
from logger_handler import log_user_activity, log_database_operations
from utils.helpers import (
admin_required,
has_admin_privileges,
has_staff_level_access,
login_required,
staff_or_admin_required)
from working_hours_calculator import WorkingHoursCalculator, round_time_to_quarter_hour, convert_minutes_to_base100, round_base100_hours
from payroll_excel_exporter import PayrollExcelExporter
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
bp = Blueprint('payroll', __name__)
@bp.route('/payroll', endpoint='payroll_dashboard')
@login_required
def payroll_dashboard():
"""Payroll dashboard for calculating and exporting working hours"""
try:
# Check if user has payroll access
user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']:
logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted to access payroll dashboard without permissions")
flash('Access denied. Only administrators and payroll staff can access payroll features.', 'error')
return redirect(url_for('dashboard.dashboard'))
logger_handler.logger.debug("Loading payroll dashboard")
# Log payroll dashboard access
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed payroll dashboard")
# Get filter parameters with defaults
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
project_filter = request.args.get('project_filter', '')
# Set default date range if not provided (last 2 weeks)
if not date_from or not date_to:
end_date = datetime.now().date()
start_date = end_date - timedelta(days=13) # 2 weeks (14 days)
date_from = start_date.strftime('%Y-%m-%d')
date_to = end_date.strftime('%Y-%m-%d')
# Get list of projects for dropdown
projects = []
try:
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
logger_handler.logger.debug(f"Found {len(projects)} active projects for payroll filter")
except Exception as e:
logger_handler.logger.warning(f"Error loading projects for payroll filter: {e}")
# Get attendance records for the period
attendance_records = []
working_hours_data = None
if date_from and date_to:
try:
start_date = datetime.strptime(date_from, '%Y-%m-%d')
end_date = datetime.strptime(date_to, '%Y-%m-%d')
# Query attendance records with optional project filter
query = db.session.query(AttendanceData).join(QRCode, AttendanceData.qr_code_id == QRCode.id)
# Apply date filter
query = query.filter(
AttendanceData.check_in_date >= start_date.date(),
AttendanceData.check_in_date <= end_date.date()
)
# Apply project filter if selected
if project_filter and project_filter != '':
query = query.filter(QRCode.project_id == int(project_filter))
logger_handler.logger.debug(f"Applied project filter: {project_filter}")
query = query.order_by(AttendanceData.employee_id, AttendanceData.check_in_date, AttendanceData.check_in_time)
attendance_records = query.all()
logger_handler.logger.debug(f"Found {len(attendance_records)} attendance records for payroll calculation")
# Calculate working hours if we have records
if attendance_records:
calculator = WorkingHoursCalculator()
working_hours_data = calculator.calculate_all_employees_hours(
start_date, end_date, attendance_records
)
logger_handler.logger.debug(f"Calculated hours for {working_hours_data['employee_count']} employees")
except ValueError as e:
logger_handler.logger.warning(f"Invalid date format in payroll dashboard: {e}")
flash('Invalid date format. Please use YYYY-MM-DD format.', 'error')
except Exception as e:
logger_handler.logger.error(f"Error calculating working hours: {e}", exc_info=True)
logger_handler.log_database_error('payroll_calculation', e)
flash('Error calculating working hours. Please check the server logs.', 'error')
# Get employee names for display
employee_names = {}
if working_hours_data:
try:
# Use parameterized IN clause to avoid SQL injection
employee_ids = list(working_hours_data['employees'].keys())
if employee_ids:
placeholders = ','.join([f':emp_{i}' for i in range(len(employee_ids))])
emp_params = {f'emp_{i}': eid for i, eid in enumerate(employee_ids)}
employee_query = db.session.execute(text(f"""
SELECT
ad.employee_id,
CONCAT(e.lastName, ',', e.firstName) as full_name
FROM attendance_data ad
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
WHERE ad.employee_id IN ({placeholders})
GROUP BY ad.employee_id, e.firstName, e.lastName
"""), emp_params)
for row in employee_query:
if row[1]: # Only add if we got a name
employee_names[str(row[0])] = row[1]
logger_handler.logger.debug(f"Retrieved names for {len(employee_names)} employees")
except Exception as e:
logger_handler.logger.warning(f"Could not load employee names: {e}", exc_info=True)
# Continue without names - will use employee IDs
# Get selected project name for display
selected_project_name = ''
if project_filter:
try:
selected_project = db.session.get(Project, int(project_filter))
if selected_project:
selected_project_name = selected_project.name
except Exception as e:
logger_handler.logger.warning(f"Error getting selected project name: {e}")
return render_template('payroll_dashboard.html',
working_hours_data=working_hours_data,
employee_names=employee_names,
projects=projects,
date_from=date_from,
date_to=date_to,
project_filter=project_filter,
selected_project_name=selected_project_name,
user_role=user_role)
except Exception as e:
db.session.rollback()
logger_handler.logger.error(f"Error loading payroll dashboard: {e}", exc_info=True)
logger_handler.log_flask_error(
'payroll_dashboard_error',
str(e),
stack_trace=traceback.format_exc()
)
flash('Error loading payroll dashboard. Please check the server logs.', 'error')
return redirect(url_for('dashboard.dashboard'))
@bp.route('/payroll/export-excel', methods=['POST'], endpoint='export_payroll_excel')
@login_required
@log_database_operations('payroll_excel_export')
def export_payroll_excel():
"""Export payroll report to Excel with working hours calculations including SP/PW support"""
try:
# Check permissions
user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']:
logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized payroll Excel export")
flash('Access denied. Only administrators and payroll staff can export payroll data.', 'error')
return redirect(url_for('payroll.payroll_dashboard'))
logger_handler.logger.info(f"Payroll Excel export started by user {session.get('username', 'unknown')}")
# Get parameters from form
date_from = request.form.get('date_from')
date_to = request.form.get('date_to')
project_filter = request.form.get('project_filter', '')
report_type = request.form.get('report_type', 'payroll') # 'payroll', 'detailed', 'template', 'enhanced', 'detailed_sp_pw'
if not date_from or not date_to:
flash('Please provide both start and end dates for the export.', 'error')
return redirect(url_for('payroll.payroll_dashboard'))
try:
start_date = datetime.strptime(date_from, '%Y-%m-%d')
end_date = datetime.strptime(date_to, '%Y-%m-%d')
except ValueError:
flash('Invalid date format. Please use YYYY-MM-DD format.', 'error')
return redirect(url_for('payroll.payroll_dashboard'))
# Get attendance records with project filter and QR code data
query = db.session.query(AttendanceData, QRCode).join(QRCode, AttendanceData.qr_code_id == QRCode.id)
# Apply date filter
query = query.filter(
AttendanceData.check_in_date >= start_date.date(),
AttendanceData.check_in_date <= end_date.date()
)
# Apply project filter if selected
if project_filter and project_filter != '':
query = query.filter(QRCode.project_id == int(project_filter))
logger_handler.logger.debug(f"Applied project filter to export: {project_filter}")
query = query.order_by(AttendanceData.employee_id, AttendanceData.check_in_date, AttendanceData.check_in_time)
# Get the results and attach QR code data to attendance records
query_results = query.all()
attendance_records = []
for attendance_data, qr_code in query_results:
# Attach the QR code object to the attendance record
attendance_data.qr_code = qr_code
attendance_records.append(attendance_data)
logger_handler.logger.info(f"Payroll export: found {len(attendance_records)} records with QR data")
if not attendance_records:
flash('No attendance records found for the selected date range and project.', 'warning')
return redirect(url_for('payroll.payroll_dashboard'))
logger_handler.logger.info(f"Exporting {len(attendance_records)} attendance records to payroll Excel")
# Get employee names using parameterized IN clause to avoid SQL injection
employee_names = {}
try:
employee_ids = list(set(str(record.employee_id) for record in attendance_records))
if employee_ids:
placeholders = ','.join([f':emp_{i}' for i in range(len(employee_ids))])
emp_params = {f'emp_{i}': eid for i, eid in enumerate(employee_ids)}
employee_query = db.session.execute(text(f"""
SELECT
ad.employee_id,
CONCAT(e.firstName, ' ', e.lastName) as full_name
FROM attendance_data ad
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
WHERE ad.employee_id IN ({placeholders})
GROUP BY ad.employee_id, e.firstName, e.lastName
"""), emp_params)
for row in employee_query:
if row[1]: # Only add if we got a name
employee_names[str(row[0])] = row[1]
logger_handler.logger.debug(f"Retrieved names for {len(employee_names)} employees for export")
except Exception as e:
logger_handler.logger.warning(f"Could not load employee names for export: {e}", exc_info=True)
# Get project name for enhanced reports and filename
project_name = None
project_name_for_filename = ''
if project_filter:
try:
project = db.session.get(Project, int(project_filter))
if project:
project_name = project.name
project_name_for_filename = f"_{project.name.replace(' ', '_')}"
except Exception as e:
logger_handler.logger.warning(f"Error getting project name for export: {e}")
# Generate Excel file based on report type
excel_file = None
filename_prefix = 'payroll_report'
if report_type == 'enhanced':
# Use enhanced exporter for SP/PW reports
logger_handler.logger.debug("Creating enhanced payroll report with SP/PW support")
try:
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'))
excel_file = exporter.create_enhanced_payroll_report(
start_date, end_date, attendance_records, employee_names, project_name
)
filename_prefix = 'enhanced_payroll_report'
logger_handler.logger.info("Enhanced payroll report created successfully")
except ImportError:
logger_handler.logger.warning("Enhanced exporter not available, falling back to standard exporter")
# Fall back to standard exporter
exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_payroll_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'payroll_report'
except Exception as e:
logger_handler.logger.warning(f"Enhanced exporter error: {e} — falling back to standard exporter")
# Fall back to standard exporter
exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_payroll_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'payroll_report'
elif report_type == 'detailed_sp_pw':
# Detailed daily SP/PW breakdown
logger_handler.logger.debug("Creating detailed SP/PW daily breakdown report")
try:
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'))
excel_file = exporter.create_detailed_sp_pw_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'detailed_sp_pw_report'
logger_handler.logger.info("Detailed SP/PW report created successfully")
except ImportError:
logger_handler.logger.warning("Enhanced exporter not available, falling back to detailed hours report")
# Fall back to standard detailed report
exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_detailed_hours_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'detailed_hours_report'
except Exception as e:
logger_handler.logger.warning(f"Enhanced exporter error: {e} — falling back to detailed hours report")
# Fall back to standard detailed report
exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_detailed_hours_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'detailed_hours_report'
else:
# Use standard exporter for existing report types
exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
if report_type == 'detailed':
excel_file = exporter.create_detailed_hours_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'detailed_hours_report'
elif report_type == 'template':
excel_file = exporter.create_template_format_report(
start_date, end_date, attendance_records, employee_names, project_name
)
filename_prefix = 'time_attendance_report'
else:
# Default payroll report
excel_file = exporter.create_payroll_report(
start_date, end_date, attendance_records, employee_names
)
filename_prefix = 'payroll_report'
if excel_file:
# Generate filename with timestamp and project name
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'{filename_prefix}_{date_from}_to_{date_to}{project_name_for_filename}_{timestamp}.xlsx'
logger_handler.logger.info(f"Payroll Excel file generated successfully: {filename}")
# Log successful export
logger_handler.logger.info(f"Payroll Excel export generated by user {session.get('username', 'unknown')}: {filename}")
if report_type == 'template':
logger_handler.logger.info(f"Template format hours export generated by user {session.get('username', 'unknown')}: {filename}")
elif report_type == 'enhanced':
logger_handler.logger.info(f"Enhanced payroll export with SP/PW generated by user {session.get('username', 'unknown')}: {filename}")
elif report_type == 'detailed_sp_pw':
logger_handler.logger.info(f"Detailed SP/PW breakdown export generated by user {session.get('username', 'unknown')}: {filename}")
return send_file(
excel_file,
as_attachment=True,
download_name=filename,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
else:
flash('Error generating payroll Excel file.', 'error')
return redirect(url_for('payroll.payroll_dashboard'))
except Exception as e:
db.session.rollback()
logger_handler.logger.error(f"Error in export_payroll_excel route: {e}", exc_info=True)
logger_handler.log_flask_error(
'payroll_excel_export_error',
str(e),
stack_trace=traceback.format_exc()
)
flash('Error generating payroll Excel export. Please check the server logs.', 'error')
return redirect(url_for('payroll.payroll_dashboard'))
@bp.route('/api/working-hours/calculate', methods=['POST'], endpoint='calculate_working_hours_api')
@login_required
@log_database_operations('working_hours_api_calculation')
def calculate_working_hours_api():
"""API endpoint for calculating working hours"""
try:
# Check permissions
user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']:
return jsonify({
'success': False,
'message': 'Access denied. Insufficient permissions.'
}), 403
# Get parameters from JSON request
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': 'No data provided'
}), 400
employee_id = data.get('employee_id')
date_from = data.get('date_from')
date_to = data.get('date_to')
if not all([employee_id, date_from, date_to]):
return jsonify({
'success': False,
'message': 'Missing required parameters: employee_id, date_from, date_to'
}), 400
try:
start_date = datetime.strptime(date_from, '%Y-%m-%d')
end_date = datetime.strptime(date_to, '%Y-%m-%d')
except ValueError:
return jsonify({
'success': False,
'message': 'Invalid date format. Use YYYY-MM-DD.'
}), 400
# Get attendance records for the employee
query = db.session.query(AttendanceData).filter(
AttendanceData.employee_id == str(employee_id),
AttendanceData.check_in_date >= start_date.date(),
AttendanceData.check_in_date <= end_date.date()
).order_by(AttendanceData.check_in_date, AttendanceData.check_in_time)
attendance_records = query.all()
# Calculate working hours using WorkingHoursCalculator
calculator = WorkingHoursCalculator()
hours_data = calculator.calculate_employee_hours(
str(employee_id), start_date, end_date, attendance_records
)
# Log API usage
logger_handler.logger.info(f"Working hours API used by {session.get('username', 'unknown')} for employee {employee_id}")
return jsonify({
'success': True,
'data': hours_data
})
except Exception as e:
db.session.rollback()
logger_handler.logger.error(f"Error in calculate_working_hours_api: {e}", exc_info=True)
logger_handler.log_flask_error(
'working_hours_api_error',
str(e),
stack_trace=traceback.format_exc()
)
return jsonify({
'success': False,
'message': 'Internal server error. Please check the server logs.'
}), 500
@bp.route('/api/employee/<employee_id>/miss-punch-details', methods=['GET'], endpoint='get_miss_punch_details')
@login_required
@log_database_operations('miss_punch_details_api')
def get_miss_punch_details(employee_id):
"""API endpoint to get detailed miss punch information for an employee"""
try:
# Check permissions
user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']:
return jsonify({
'success': False,
'message': 'Access denied. Insufficient permissions.'
}), 403
# Get date parameters from query string (from the current payroll filters)
date_from = request.args.get('date_from')
date_to = request.args.get('date_to')
project_filter = request.args.get('project_filter', '')
if not all([date_from, date_to]):
return jsonify({
'success': False,
'message': 'Missing required parameters: date_from, date_to'
}), 400
try:
start_date = datetime.strptime(date_from, '%Y-%m-%d')
end_date = datetime.strptime(date_to, '%Y-%m-%d')
except ValueError:
return jsonify({
'success': False,
'message': 'Invalid date format. Use YYYY-MM-DD.'
}), 400
# Get employee name using proper firstName and lastName fields
try:
employee_query = db.session.execute(text("""
SELECT e.id,
CONCAT(e.firstName, ' ', e.lastName) as full_name
FROM employee e
WHERE e.id = :emp_id
"""), {'emp_id': int(employee_id)})
employee_row = employee_query.fetchone()
employee_name = employee_row.full_name if employee_row and employee_row.full_name else f"Employee {employee_id}"
logger_handler.logger.debug(f"Retrieved employee name for ID {employee_id}: {employee_name}")
except Exception as e:
logger_handler.logger.warning(f"Could not load employee name for ID {employee_id}: {e}", exc_info=True)
employee_name = f"Employee {employee_id}"
# Get attendance records for the employee within the period
query = db.session.query(AttendanceData).filter(
AttendanceData.employee_id == str(employee_id),
AttendanceData.check_in_date >= start_date.date(),
AttendanceData.check_in_date <= end_date.date()
)
# Apply project filter if provided
if project_filter:
try:
project_id = int(project_filter)
query = query.join(QRCode, AttendanceData.qr_code_id == QRCode.id) \
.filter(QRCode.project_id == project_id)
except ValueError:
pass # Invalid project_id, ignore filter
attendance_records = query.order_by(
AttendanceData.check_in_date,
AttendanceData.check_in_time
).all()
# Convert to the format expected by the calculator
converted_records = []
for record in records:
# Get distance from the TimeAttendance record
distance_value = getattr(record, 'distance', None)
converted_record = type('Record', (), {
'id': record.id,
'employee_id': str(record.employee_id),
'check_in_date': record.attendance_date,
'check_in_time': record.attendance_time,
'location_name': record.location_name,
'latitude': None,
'longitude': None,
'distance': distance_value, # ADD THIS LINE
'qr_code': type('QRCode', (), {
'location': record.location_name,
'location_address': record.recorded_address or '',
'project': None
})()
})()
converted_records.append(converted_record)
# Calculate working hours using the same calculator as the dashboard
# Calculate hours for this employee
hours_data = calculator.calculate_employee_hours(
str(employee_id), start_date, end_date, converted_records
)
# Extract miss punch details
miss_punch_days = []
if 'daily_hours' in hours_data:
for date_str, day_data in hours_data['daily_hours'].items():
if day_data.get('is_miss_punch', False):
# Get the actual records for this day
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_records = [r for r in converted_records if r.check_in_date == date_obj]
# Format the records information with event types
record_details = []
for i, record in enumerate(day_records):
# Determine event type based on position (alternating check-in/check-out)
# First record is always check-in, then alternates
event_type = "Check In" if i % 2 == 0 else "Check Out"
record_details.append({
'time': record.check_in_time.strftime('%H:%M:%S'),
'event_type': event_type,
'location': record.location_name or 'Unknown Location',
'has_gps': record.latitude is not None and record.longitude is not None
})
miss_punch_days.append({
'date': date_str,
'date_formatted': datetime.strptime(date_str, '%Y-%m-%d').strftime('%B %d, %Y (%A)'),
'records_count': day_data.get('records_count', 0),
'records': record_details,
'reason': 'Incomplete punch pairs - missing check-in or check-out' if len(
day_records) % 2 != 0 else 'Invalid work period duration'
})
# Log the API access
logger_handler.logger.info(
f"Miss punch details API accessed by {session.get('username', 'unknown')} for employee {employee_id}")
return jsonify({
'success': True,
'data': {
'employee_id': employee_id,
'employee_name': employee_name,
'period': f"{date_from} to {date_to}",
'miss_punch_count': len(miss_punch_days),
'miss_punch_days': miss_punch_days
}
})
except Exception as e:
db.session.rollback()
logger_handler.logger.error(f"Error in get_miss_punch_details: {e}", exc_info=True)
logger_handler.log_flask_error(
'miss_punch_details_api_error',
str(e),
stack_trace=traceback.format_exc()
)
return jsonify({
'success': False,
'message': 'Internal server error. Please check the server logs.'
}), 500
+1 -10
View File
@@ -78,10 +78,6 @@
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
<span class="menu-text">Time Attendance</span> <span class="menu-text">Time Attendance</span>
</a> </a>
<a href="{{ url_for('payroll.payroll_dashboard') }}" class="menu-item">
<i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span>
</a>
<a href="{{ url_for('statistics.qr_statistics') }}" <a href="{{ url_for('statistics.qr_statistics') }}"
class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}"> class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}">
<i class="fas fa-chart-pie"></i> <i class="fas fa-chart-pie"></i>
@@ -111,12 +107,7 @@
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
<span class="menu-text">Time Attendance</span> <span class="menu-text">Time Attendance</span>
</a> </a>
<!--
<a href="{{ url_for('payroll.payroll_dashboard') }}" class="menu-item">
<i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span>
</a>
-->
{% elif session.role in ['project_manager'] %} {% elif session.role in ['project_manager'] %}
<a href="{{ url_for('attendance.attendance_report') }}" class="menu-item"> <a href="{{ url_for('attendance.attendance_report') }}" class="menu-item">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
File diff suppressed because it is too large Load Diff