04/29 Removed payroll functionality from the app
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
@@ -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
Reference in New Issue
Block a user