From a4405e2aa08cad23c50b8f032e1f09d528aafc9c Mon Sep 17 00:00:00 2001 From: NguyenND Date: Wed, 29 Apr 2026 16:21:05 -0400 Subject: [PATCH] 04/29 Removed payroll functionality from the app --- app.py | 3 +- enhanced_payroll_excel_exporter.py | 391 --------- payroll_excel_exporter.py | 1132 -------------------------- routes/payroll.py | 655 --------------- templates/base_authenticated.html | 11 +- templates/payroll_dashboard.html | 1188 ---------------------------- 6 files changed, 2 insertions(+), 3378 deletions(-) delete mode 100644 enhanced_payroll_excel_exporter.py delete mode 100644 payroll_excel_exporter.py delete mode 100644 routes/payroll.py delete mode 100644 templates/payroll_dashboard.html diff --git a/app.py b/app.py index 90c2abb..927b777 100644 --- a/app.py +++ b/app.py @@ -88,13 +88,12 @@ def create_app() -> Flask: import routes.attendance_edit # noqa: F401 import routes.verification # 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.employees import bp as employees_bp from routes.time_attendance import bp as time_attendance_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): app.register_blueprint(bp) diff --git a/enhanced_payroll_excel_exporter.py b/enhanced_payroll_excel_exporter.py deleted file mode 100644 index 204a3bd..0000000 --- a/enhanced_payroll_excel_exporter.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/payroll_excel_exporter.py b/payroll_excel_exporter.py deleted file mode 100644 index fdf0b69..0000000 --- a/payroll_excel_exporter.py +++ /dev/null @@ -1,1132 +0,0 @@ -#!/usr/bin/env python3 -""" -Payroll Excel Exporter -===================== - -Enhanced Excel exporter for payroll reports with working hours calculations. -Based on the Java PayrollReport.java implementation. - -Features: -- Employee working hours with daily/weekly breakdown -- Regular and overtime hour calculations -- Travel time inclusion options -- Professional Excel formatting -- Multiple report formats -""" - -import io -from datetime import datetime, timedelta -from typing import List, Dict, Any, Optional -from openpyxl import Workbook -from openpyxl.styles import Font, Alignment, PatternFill, Border, Side -from openpyxl.utils import get_column_letter -from working_hours_calculator import WorkingHoursCalculator -from logger_handler import log_database_operations - - -class PayrollExcelExporter: - """Excel exporter for payroll reports with working hours""" - - def __init__(self, company_name: str = "Company Name", contract_name: str = "Default Contract"): - self.company_name = company_name - self.contract_name = contract_name - - # Excel styling - self.header1_style = None - self.header2_style = None - self.header3_style = None - self.table_header_style = None - self.data_style = None - self.total_style = None - - def _setup_styles(self, workbook: Workbook): - """Setup Excel cell styles""" - - # Header 1 style (Company name) - self.header1_style = { - 'font': Font(name="Arial", size=16, bold=True, color="FFFFFF"), - 'fill': PatternFill(start_color="2F4F4F", end_color="2F4F4F", fill_type="solid"), - 'alignment': Alignment(horizontal="center", vertical="center") - } - - # Header 2 style (Report title) - self.header2_style = { - 'font': Font(name="Arial", size=14, bold=True, color="FFFFFF"), - 'fill': PatternFill(start_color="4682B4", end_color="4682B4", fill_type="solid"), - 'alignment': Alignment(horizontal="center", vertical="center") - } - - # Header 3 style (Contract and date range) - self.header3_style = { - 'font': Font(name="Arial", size=12, bold=True), - 'alignment': Alignment(horizontal="center", vertical="center") - } - - # Table header style - self.table_header_style = { - 'font': Font(name="Arial", size=11, bold=True, color="FFFFFF"), - 'fill': PatternFill(start_color="366092", end_color="366092", 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 style - 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") - ) - } - - # Total style - self.total_style = { - 'font': Font(name="Arial", size=11, bold=True), - 'fill': PatternFill(start_color="E6E6FA", end_color="E6E6FA", fill_type="solid"), - 'alignment': Alignment(horizontal="center", vertical="center"), - 'border': Border( - left=Side(style="thick"), - right=Side(style="thick"), - top=Side(style="thick"), - bottom=Side(style="thick") - ) - } - - def _apply_style(self, cell, style_dict): - """Apply a style dictionary to a cell""" - for attr, value in style_dict.items(): - setattr(cell, attr, value) - - def _get_employee_names(self, attendance_records: List[Dict]) -> Dict[str, str]: - """Get employee names - fallback method if names not provided""" - print("โš ๏ธ Excel exporter falling back to internal name lookup - this should not normally happen") - return {} # Return empty dict - names should be provided by the route - - def _get_employee_names(self, attendance_records: List[Dict]) -> Dict[str, str]: - """Get employee names using the same CAST method as attendance report""" - employee_names = {} - try: - from flask import current_app - from sqlalchemy import text - - # Get unique employee IDs from attendance records - employee_ids = [] - for record in attendance_records: - if hasattr(record, '__dict__'): - emp_id = str(record.employee_id) - else: - emp_id = str(record['employee_id']) - - if emp_id not in employee_ids: - employee_ids.append(emp_id) - - if employee_ids: - # Use the same SQL approach as attendance report - JOIN with CAST - placeholders = ','.join([f"'{emp_id}'" for emp_id in employee_ids]) - - # Import db from current app context - from app import db - - 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 - """)) - - for row in employee_query: - if row[1]: # Only add if we got a name - employee_names[str(row[0])] = row[1] - - print(f"๐Ÿ“Š Excel exporter retrieved names for {len(employee_names)} employees using CAST method") - - except Exception as e: - print(f"โš ๏ธ Excel exporter could not load employee names: {e}") - import traceback - print(f"โš ๏ธ Traceback: {traceback.format_exc()}") - - return employee_names - - @log_database_operations('payroll_excel_export') - def create_payroll_report(self, start_date: datetime, end_date: datetime, - attendance_records: List[Dict], employee_names: Dict[str, str] = None) -> io.BytesIO: - """ - Create a comprehensive payroll report with working 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 - - Returns: - BytesIO buffer containing the Excel file - """ - try: - print(f"๐Ÿ“Š Creating payroll report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") - - # Get employee names using the same method as payroll routes - if employee_names is None: - employee_names = self._get_employee_names(attendance_records) - - # Calculate working hours - calculator = WorkingHoursCalculator() - hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records) - - # Create workbook - workbook = Workbook() - worksheet = workbook.active - worksheet.title = "Payroll Report" - - # Setup styles - self._setup_styles(workbook) - - # Write report headers - current_row = self._write_report_headers(worksheet, start_date, end_date) - - # Write employee data - current_row = self._write_employee_payroll_data(worksheet, hours_data, employee_names, current_row) - - # Write summary totals - self._write_summary_totals(worksheet, hours_data, current_row) - - # Auto-adjust column widths - self._auto_adjust_columns(worksheet) - - # Save to BytesIO - excel_buffer = io.BytesIO() - workbook.save(excel_buffer) - excel_buffer.seek(0) - - print("โœ… Payroll report Excel file created successfully") - return excel_buffer - - except Exception as e: - print(f"โŒ Error creating payroll report: {e}") - raise e - - def _write_report_headers(self, worksheet, start_date: datetime, end_date: datetime) -> int: - """Write report headers and return next row number""" - current_row = 1 - - # Company name - cell = worksheet.cell(row=current_row, column=1, value=self.company_name) - self._apply_style(cell, self.header1_style) - worksheet.merge_cells(f'A{current_row}:Q{current_row}') - current_row += 1 - - # Report title - cell = worksheet.cell(row=current_row, column=1, value="Payroll Report - Working Hours Summary") - self._apply_style(cell, self.header2_style) - worksheet.merge_cells(f'A{current_row}:Q{current_row}') - current_row += 1 - - # Contract name - cell = worksheet.cell(row=current_row, column=1, value=self.contract_name) - self._apply_style(cell, self.header3_style) - worksheet.merge_cells(f'A{current_row}:Q{current_row}') - current_row += 1 - - # Date range - date_range = f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}" - cell = worksheet.cell(row=current_row, column=1, value=date_range) - self._apply_style(cell, self.header3_style) - worksheet.merge_cells(f'A{current_row}:Q{current_row}') - current_row += 2 # Add extra space - - return current_row - - def _write_employee_payroll_data(self, worksheet, hours_data: Dict, employee_names: Dict[str, str], start_row: int) -> int: - """Write employee payroll data and return next row number""" - current_row = start_row - - # Table headers - headers = [ - "#", "Employee ID", "Employee Name", - "Week 1 Mon", "Week 1 Tue", "Week 1 Wed", "Week 1 Thu", "Week 1 Fri", "Week 1 Sat", "Week 1 Sun", - "Week 1 Regular", "Week 1 OT", - "Week 2 Mon", "Week 2 Tue", "Week 2 Wed", "Week 2 Thu", "Week 2 Fri", "Week 2 Sat", "Week 2 Sun", - "Week 2 Regular", "Week 2 OT", - "Total Regular", "Total OT", "Grand Total" - ] - - for col, header in enumerate(headers, 1): - cell = worksheet.cell(row=current_row, column=col, value=header) - self._apply_style(cell, self.table_header_style) - current_row += 1 - - # Employee data - counter = 1 - for employee_id, emp_data in hours_data['employees'].items(): - # Employee basic info - employee_name = employee_names.get(employee_id, f"Employee {employee_id}") if employee_names else f"Employee {employee_id}" - - row_data = [ - counter, - employee_id, - employee_name - ] - - # Calculate daily hours for two weeks (14 days) - daily_hours = self._calculate_daily_hours_for_payroll(emp_data, hours_data['period_start']) - - # Week 1 daily hours (7 days) - week1_total = 0 - for day_idx in range(7): - hours = daily_hours.get(day_idx, 0) - row_data.append(round(hours, 2)) - week1_total += hours - - # Week 1 regular and OT - week1_regular = min(week1_total, 40.0) - week1_ot = max(0, week1_total - 40.0) - row_data.extend([round(week1_regular, 2), round(week1_ot, 2)]) - - # Week 2 daily hours (7 days) - week2_total = 0 - for day_idx in range(7, 14): - hours = daily_hours.get(day_idx, 0) - row_data.append(round(hours, 2)) - week2_total += hours - - # Week 2 regular and OT - week2_regular = min(week2_total, 40.0) - week2_ot = max(0, week2_total - 40.0) - row_data.extend([round(week2_regular, 2), round(week2_ot, 2)]) - - # Totals - total_regular = week1_regular + week2_regular - total_ot = week1_ot + week2_ot - grand_total = total_regular + total_ot - row_data.extend([round(total_regular, 2), round(total_ot, 2), round(grand_total, 2)]) - - # Write row data - 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) - - current_row += 1 - counter += 1 - - return current_row + 1 # Add space before summary - - def _calculate_daily_hours_for_payroll(self, emp_data: Dict, start_date_str: str) -> Dict[int, float]: - """Calculate daily hours for payroll format (14 days)""" - daily_hours = {} - start_date = datetime.strptime(start_date_str, '%Y-%m-%d') - - for day_idx in range(14): - current_date = start_date + timedelta(days=day_idx) - date_key = current_date.strftime('%Y-%m-%d') - - if date_key in emp_data['daily_hours']: - day_data = emp_data['daily_hours'][date_key] - if not day_data['is_miss_punch']: - daily_hours[day_idx] = day_data['total_hours'] - else: - daily_hours[day_idx] = 0 # Miss punch = 0 hours - else: - daily_hours[day_idx] = 0 # No records = 0 hours - - return daily_hours - - def _write_summary_totals(self, worksheet, hours_data: Dict, start_row: int): - """Write summary totals section""" - current_row = start_row + 1 - - # Summary header - cell = worksheet.cell(row=current_row, column=1, value="PAYROLL SUMMARY") - self._apply_style(cell, self.header2_style) - worksheet.merge_cells(f'A{current_row}:F{current_row}') - current_row += 2 - - # Calculate totals across all employees - total_employees = len(hours_data['employees']) - total_regular_hours = 0 - total_overtime_hours = 0 - total_hours = 0 - - for emp_data in hours_data['employees'].values(): - total_regular_hours += emp_data['grand_totals']['regular_hours'] - total_overtime_hours += emp_data['grand_totals']['overtime_hours'] - total_hours += emp_data['grand_totals']['total_hours'] - - # Summary data - summary_data = [ - ("Total Employees:", total_employees), - ("Total Regular Hours:", round(total_regular_hours, 2)), - ("Total Overtime Hours:", round(total_overtime_hours, 2)), - ("Grand Total Hours:", round(total_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.total_style) - cell2 = worksheet.cell(row=current_row, column=2, value=value) - self._apply_style(cell2, self.total_style) - current_row += 1 - - 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, 20) - worksheet.column_dimensions[column_letter].width = adjusted_width - - @log_database_operations('detailed_hours_export') - def create_detailed_hours_report(self, start_date: datetime, end_date: datetime, - attendance_records: List[Dict], employee_names: Dict[str, str] = None) -> io.BytesIO: - """ - Create a detailed daily hours report for all employees - - 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 - - Returns: - BytesIO buffer containing the Excel file - """ - try: - print(f"๐Ÿ“Š Creating detailed hours report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") - - # Get employee names using the same method as payroll routes - if employee_names is None: - employee_names = self._get_employee_names(attendance_records) - - # Calculate working hours - calculator = WorkingHoursCalculator() - hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records) - - # Create workbook - workbook = Workbook() - worksheet = workbook.active - worksheet.title = "Detailed Hours Report" - - # Setup styles - self._setup_styles(workbook) - - # Write headers - current_row = 1 - cell = worksheet.cell(row=current_row, column=1, value=self.company_name) - self._apply_style(cell, self.header1_style) - worksheet.merge_cells(f'A{current_row}:J{current_row}') - current_row += 1 - - cell = worksheet.cell(row=current_row, column=1, value="Detailed Daily Hours Report") - self._apply_style(cell, self.header2_style) - worksheet.merge_cells(f'A{current_row}:J{current_row}') - current_row += 2 - - # Table headers - headers = ["Employee ID", "Employee Name", "Date", "Day of Week", "Total Hours", - "Status", "Records Count", "Regular Hours", "Overtime Hours", "Notes"] - - for col, header in enumerate(headers, 1): - cell = worksheet.cell(row=current_row, column=col, value=header) - self._apply_style(cell, self.table_header_style) - current_row += 1 - - # Write detailed data - for employee_id, emp_data in hours_data['employees'].items(): - employee_name = employee_names.get(employee_id, f"Employee {employee_id}") if employee_names else f"Employee {employee_id}" - - # Sort daily hours by date - daily_items = sorted(emp_data['daily_hours'].items()) - - for date_str, day_data in daily_items: - date_obj = datetime.strptime(date_str, '%Y-%m-%d') - day_name = date_obj.strftime('%A') - - # Determine status and notes - if day_data['is_miss_punch']: - status = "Miss Punch" - notes = "Missing check-in or check-out" - hours = 0 - elif day_data['records_count'] == 0: - status = "No Records" - notes = "No attendance records" - hours = 0 - else: - status = "Complete" - notes = f"{day_data['records_count']} record(s)" - hours = day_data['total_hours'] - - # Calculate regular/overtime for this day - regular_hours = min(hours, 8.0) # Daily limit - overtime_hours = max(0, hours - 8.0) - - row_data = [ - employee_id, - employee_name, - date_str, - day_name, - round(hours, 2), - status, - day_data['records_count'], - round(regular_hours, 2), - round(overtime_hours, 2), - 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 code miss punches - if status == "Miss Punch": - cell.fill = PatternFill(start_color="FFE6E6", end_color="FFE6E6", fill_type="solid") - elif status == "No Records": - cell.fill = PatternFill(start_color="F0F0F0", end_color="F0F0F0", 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 hours report Excel file created successfully") - return excel_buffer - - except Exception as e: - print(f"โŒ Error creating detailed hours report: {e}") - raise e - - @log_database_operations('template_hours_export') - def create_template_format_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 a template-format report matching the provided Excel template. - This creates a single sheet with all employees' detailed reports. - - 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 header - - Returns: - BytesIO buffer containing the Excel file - """ - try: - print(f"๐Ÿ“Š Creating single-sheet template format report from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") - - # Get employee names using the same method as payroll routes - if employee_names is None: - employee_names = self._get_employee_names(attendance_records) - - # Calculate working hours - calculator = WorkingHoursCalculator() - hours_data = calculator.calculate_all_employees_hours(start_date, end_date, attendance_records) - - # Create workbook with single sheet - workbook = Workbook() - worksheet = workbook.active - worksheet.title = "Hours Report" - - # Setup styles - self._setup_template_styles(workbook) - - # Write main report headers once at the top - current_row = self._write_main_report_headers(worksheet, start_date, end_date, project_name) - - # Write all employees data in one sheet - for employee_id, emp_data in hours_data['employees'].items(): - employee_name = employee_names.get(employee_id, f"Employee {employee_id}") if employee_names else f"Employee {employee_id}" - - # Write employee section - current_row = self._write_employee_section(worksheet, employee_id, employee_name, emp_data, - start_date, end_date, attendance_records, current_row) - - # Add spacing between employees - current_row += 2 - - # Auto-adjust column widths to match template - self._adjust_template_columns(worksheet) - - # Save to BytesIO - excel_buffer = io.BytesIO() - workbook.save(excel_buffer) - excel_buffer.seek(0) - - print("โœ… Single-sheet template format report Excel file created successfully") - return excel_buffer - - except Exception as e: - print(f"โŒ Error creating template format report: {e}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") - raise e - - def _setup_template_styles(self, workbook: Workbook): - """Setup Excel cell styles for template format""" - - # Company name style (matches template) - self.template_company_style = { - 'font': Font(name="Arial", size=14, bold=True), - 'alignment': Alignment(horizontal="left", vertical="center") - } - - # Report title style - self.template_title_style = { - 'font': Font(name="Arial", size=12, bold=True), - 'alignment': Alignment(horizontal="left", vertical="center") - } - - # Project name style - self.template_project_style = { - 'font': Font(name="Arial", size=11, bold=True), - 'alignment': Alignment(horizontal="left", vertical="center") - } - - # Date range style - self.template_date_style = { - 'font': Font(name="Arial", size=11), - 'alignment': Alignment(horizontal="left", vertical="center") - } - - # Employee info style - self.template_employee_style = { - 'font': Font(name="Arial", size=11, bold=True), - 'alignment': Alignment(horizontal="left", vertical="center") - } - - # Column header style - self.template_header_style = { - 'font': Font(name="Arial", size=10, bold=True), - 'fill': PatternFill(start_color="D9E1F2", end_color="D9E1F2", 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 cell style - self.template_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") - ) - } - - def _adjust_template_columns(self, worksheet): - """Adjust column widths to match template format""" - # Column widths based on template analysis - column_widths = { - 'A': 12, # Day - 'B': 10.6, # Date - 'C': 12.6, # In - 'D': 12.6, # Out - 'E': 34, # Location - 'F': 5.6, # Zone - 'G': 15.4, # Hours/Building - 'H': 10.9, # Daily Total - 'I': 14.0, # Regular Hours - 'J': 9.6, # OT Hours - 'K': 35, # Building Address - 'L': 35, # Recorded Location - 'M': 10.0, # Distance - 'N': 15.0, # Possible Violation - } - - for col_letter, width in column_widths.items(): - worksheet.column_dimensions[col_letter].width = width - - def _write_main_report_headers(self, worksheet, start_date: datetime, end_date: datetime, project_name: str) -> int: - """Write main report headers at the top of the sheet""" - current_row = 1 - - # 1. Company Name (merged across columns A-N) - cell = worksheet.cell(row=current_row, column=1, value=self.company_name) - self._apply_style(cell, self.template_company_style) - worksheet.merge_cells(f'A{current_row}:N{current_row}') - current_row += 1 - - # 2. Report title (merged across columns A-N) - cell = worksheet.cell(row=current_row, column=1, value="Summary report of Hours worked") - self._apply_style(cell, self.template_title_style) - worksheet.merge_cells(f'A{current_row}:N{current_row}') - current_row += 1 - - # 3. Project name (merged across columns A-N) - project_display = project_name if project_name else "[Project Name]" - cell = worksheet.cell(row=current_row, column=1, value=project_display) - self._apply_style(cell, self.template_project_style) - worksheet.merge_cells(f'A{current_row}:N{current_row}') - current_row += 1 - - # 4. Date range (merged across columns A-N) - date_range = f"Date range: {start_date.strftime('%m/%d/%Y')} to {end_date.strftime('%m/%d/%Y')}" - cell = worksheet.cell(row=current_row, column=1, value=date_range) - self._apply_style(cell, self.template_date_style) - worksheet.merge_cells(f'A{current_row}:N{current_row}') - current_row += 2 # Add extra space - - return current_row - - def _write_employee_section(self, worksheet, employee_id: str, employee_name: str, - emp_data: Dict, start_date: datetime, end_date: datetime, - attendance_records: List[Dict], start_row: int) -> int: - """Write individual employee section with FIXED WEEKLY OVERTIME CALCULATION""" - current_row = start_row - - # Employee info header (merged across columns A-O) - employee_info = f"Employee ID {employee_id}: {employee_name}" - cell = worksheet.cell(row=current_row, column=1, value=employee_info) - self._apply_style(cell, self.template_employee_style) - worksheet.merge_cells(f'A{current_row}:N{current_row}') - current_row += 1 - - # Column headers for this employee - headers = ["Day", "Date", "In", "Out", "Location", "Zone", "Hours/Building", - "Daily Total", "Regular Hours", "OT Hours", "Building Address", - "Recorded Location", "Distance", "Possible Violation"] - - for col, header in enumerate(headers, 1): - cell = worksheet.cell(row=current_row, column=col, value=header) - self._apply_style(cell, self.template_header_style) - current_row += 1 - - # Get attendance records for this employee for location info - employee_records = [record for record in attendance_records if str(record.employee_id) == employee_id] - - # Group attendance records by date for location info - daily_location_data = {} - for record in employee_records: - date_key = record.check_in_date.strftime('%Y-%m-%d') - if date_key not in daily_location_data: - daily_location_data[date_key] = { - 'records': [], - 'location': '', - 'building_address': '' - } - daily_location_data[date_key]['records'].append(record) - - # --------------------------------------------------------------- - # OVERNIGHT SHIFT DETECTION (display layer) - # Mirror the same logic used in working_hours_calculator so that - # the In/Out times rendered in the Excel rows are consistent with - # the computed hours: late check-in (>= 18:00) on Day N paired with - # early check-out (<= 06:00) on Day N+1 โ†’ move check-out to Day N. - # --------------------------------------------------------------- - from datetime import time as time_type - OVERNIGHT_CHECKIN_HOUR = 18 - OVERNIGHT_CHECKOUT_HOUR = 6 - - sorted_dates = sorted(daily_location_data.keys()) - for idx, date_key in enumerate(sorted_dates): - day_records = daily_location_data[date_key]['records'] - check_ins = [r for r in day_records if getattr(r, 'record_type', 'check_in') == 'check_in'] - check_outs = [r for r in day_records if getattr(r, 'record_type', 'check_in') == 'check_out'] - - unmatched_late_ins = [] - for ci in check_ins: - ci_hour = ci.check_in_time.hour if isinstance(ci.check_in_time, time_type) else 0 - if ci_hour >= OVERNIGHT_CHECKIN_HOUR and len(check_outs) < len(check_ins): - unmatched_late_ins.append(ci) - - if not unmatched_late_ins or idx + 1 >= len(sorted_dates): - continue - - next_date_key = sorted_dates[idx + 1] - day_n = datetime.strptime(date_key, '%Y-%m-%d').date() - day_n1 = datetime.strptime(next_date_key, '%Y-%m-%d').date() - if (day_n1 - day_n).days != 1: - continue - - next_day_records = daily_location_data[next_date_key]['records'] - next_check_ins = [r for r in next_day_records if getattr(r, 'record_type', 'check_in') == 'check_in'] - next_check_outs = [r for r in next_day_records if getattr(r, 'record_type', 'check_in') == 'check_out'] - - orphaned_early_outs = [] - for co in next_check_outs: - co_hour = co.check_in_time.hour if isinstance(co.check_in_time, time_type) else 0 - if co_hour <= OVERNIGHT_CHECKOUT_HOUR and len(next_check_ins) < len(next_check_outs): - orphaned_early_outs.append(co) - - for co in orphaned_early_outs[:len(unmatched_late_ins)]: - print(f"๐ŸŒ™ [Exporter] Overnight shift: moving check-out {co.check_in_time} " - f"from {next_date_key} โ†’ {date_key} for employee {employee_id}") - daily_location_data[date_key]['records'].append(co) - daily_location_data[next_date_key]['records'].remove(co) - - if not daily_location_data[next_date_key]['records']: - del daily_location_data[next_date_key] - # --------------------------------------------------------------- - # END OVERNIGHT SHIFT DETECTION (display layer) - # --------------------------------------------------------------- - - # Get location info for each day - for date_str, day_info in daily_location_data.items(): - sorted_records = sorted(day_info['records'], key=lambda x: x.check_in_time) - if sorted_records: - # Get location info from QR code - if hasattr(sorted_records[0], 'qr_code') and sorted_records[0].qr_code: - day_info['location'] = sorted_records[0].qr_code.location or '' - day_info['building_address'] = sorted_records[0].qr_code.location_address or '' - - # FIXED: Initialize ALL weekly totals tracking variables - weekly_total_hours = 0 - - # Track overall totals for grand total - grand_regular_hours = 0 - grand_ot_hours = 0 - grand_total_hours = 0 - total_pairs_written = 0 # Track total pairs across all days - - # Track week boundaries (assuming payroll period starts on Monday) - current_week_start = None - - # Log the overtime calculation change - print(f"๐Ÿ”ง FIXED: Using weekly overtime calculation (40 hrs/week) for employee {employee_id}") - - # Write data rows using the calculated daily hours from emp_data - for date_str in sorted(emp_data['daily_hours'].keys()): - date_obj = datetime.strptime(date_str, '%Y-%m-%d') - day_hours_data = emp_data['daily_hours'][date_str] - - # Skip days with miss punch, but INCLUDE days with valid hours even if single period - if day_hours_data.get('is_miss_punch', False): - print(f"โš ๏ธ Skipping {date_str} - Miss punch detected") - continue - - # Include days with valid working hours (single periods should show Daily Total) - if day_hours_data.get('total_hours', 0) <= 0: - print(f"โš ๏ธ Skipping {date_str} - No working hours") - continue - - print(f"โœ… Including {date_str} - Working hours: {day_hours_data.get('total_hours', 0):.2f}") - - # Calculate week boundaries - week_start = date_obj - timedelta(days=date_obj.weekday()) # Monday of the week - - # FIXED: Check if we've moved to a new week and need to write weekly total - if current_week_start is not None and week_start != current_week_start: - # Calculate weekly overtime using 40-hour threshold - week_regular = min(weekly_total_hours, 40.0) - week_overtime = max(0, weekly_total_hours - 40.0) - - # Write weekly total row for previous week - current_row = self._write_weekly_total_row(worksheet, current_row, - week_regular, week_overtime, weekly_total_hours) - - # Add to grand totals - grand_regular_hours += week_regular - grand_ot_hours += week_overtime - - # Reset weekly counters - weekly_total_hours = 0 - - current_week_start = week_start - - total_hours = day_hours_data.get('total_hours', 0) - - # FIXED: Remove daily overtime calculation - now calculated weekly only - # OLD CODE: regular_hours = min(total_hours, 8.0) # Max 8 regular hours per day - # OLD CODE: ot_hours = max(0, total_hours - 8.0) - - # Add to weekly totals (no daily overtime calculation) - weekly_total_hours += total_hours - grand_total_hours += total_hours - - # Get location info for this date - location_info = daily_location_data.get(date_str, {}) - location = location_info.get('location', '') - building_address = location_info.get('building_address', '') - - # Get all records for this day, sorted by time - day_records = [] - if date_str in daily_location_data: - day_records = sorted(daily_location_data[date_str]['records'], key=lambda x: x.check_in_time) - - # Write multiple pairs for the day (like template shows FRIDAY appearing twice) - pairs_written = 0 - if len(day_records) >= 2: - # Calculate total number of pairs for this day - total_pairs_for_day = len(day_records) // 2 - print(f"๐Ÿ“Š Day {date_str}: {len(day_records)} records = {total_pairs_for_day} pairs") - - # Create pairs from consecutive records - for i in range(0, len(day_records) - 1, 2): - if i + 1 < len(day_records): - start_record = day_records[i] - end_record = day_records[i + 1] - - # FIXED: Pass total_hours without daily overtime split - current_row = self._write_record_pair_row( - worksheet, current_row, date_obj, start_record, end_record, - location, building_address, total_hours, 0, 0, # No daily regular/ot split - pairs_written, total_hours, total_pairs_for_day - ) - pairs_written += 1 - total_pairs_written += 1 # Track total pairs - else: - # Single record or no records - write one row - start_record = day_records[0] if day_records else None - current_row = self._write_single_record_row( - worksheet, current_row, date_obj, start_record, - location, building_address, total_hours, 0, 0 # No daily regular/ot split - ) - - # FIXED: Write final weekly total with proper weekly overtime calculation - if weekly_total_hours > 0: - week_regular = min(weekly_total_hours, 40.0) - week_overtime = max(0, weekly_total_hours - 40.0) - current_row = self._write_weekly_total_row(worksheet, current_row, - week_regular, week_overtime, weekly_total_hours) - - # Add final week to grand totals - grand_regular_hours += week_regular - grand_ot_hours += week_overtime - - # Write grand total row - current_row = self._write_grand_total_row(worksheet, current_row, - grand_regular_hours, grand_ot_hours) - - # Add space between employees - current_row += 2 - - # Log the export action with fixed overtime calculation - print(f"โœ… FIXED Template format export: Employee {employee_id} ({employee_name}) data written with weekly overtime calculation (40 hrs/week)") - print(f"๐Ÿ“Š Total: {grand_total_hours:.2f} hrs, Regular: {grand_regular_hours:.2f} hrs, Overtime: {grand_ot_hours:.2f} hrs") - - return current_row - - def _write_record_pair_row(self, worksheet, current_row: int, date_obj: datetime, - start_record, end_record, location: str, building_address: str, - day_total_hours: float, regular_hours: float, ot_hours: float, - pair_index: int, daily_total_hours: float, total_pairs_for_day: int = 1) -> int: - """Write a single record pair row (in/out times)""" - day_name = date_obj.strftime('%A').upper() - date_str = date_obj.strftime('%m/%d/%Y') - - # Calculate hours for this specific pair - if start_record and end_record: - start_time = start_record.check_in_time - end_time = end_record.check_in_time - start_datetime = datetime.combine(start_record.check_in_date, start_time) - end_datetime = datetime.combine(end_record.check_in_date, end_time) - pair_duration = (end_datetime - start_datetime).total_seconds() / 3600 - pair_hours = round(pair_duration, 2) - else: - start_time = start_record.check_in_time if start_record else None - end_time = end_record.check_in_time if end_record else None - pair_hours = 0 - - # Format times - check_in_time = start_time.strftime('%I:%M:%S %p') if start_time else '' - check_out_time = end_time.strftime('%I:%M:%S %p') if end_time else '' - - # Get location accuracy info - location_accuracy = None - recorded_location = '' - distance_value = '' - possible_violation = 'No' - - if start_record and hasattr(start_record, 'location_accuracy'): - location_accuracy = getattr(start_record, 'location_accuracy', None) - if location_accuracy is not None: - try: - accuracy_value = float(location_accuracy) - distance_value = f"{accuracy_value:.3f}" - - if accuracy_value < 0.3: - recorded_location = building_address - possible_violation = "No" - else: - recorded_location = f"GPS Location (Accuracy: {distance_value} miles)" - possible_violation = "Yes" if accuracy_value > 0.5 else "Possible" - except (ValueError, TypeError): - distance_value = "Unknown" - recorded_location = "GPS Location (Unknown Accuracy)" - possible_violation = "Possible" - - # Create building address hyperlink if available - building_address_display = building_address - if building_address and "," in building_address: - # Create Google Maps hyperlink - maps_url = f"https://www.google.com/maps/place/{building_address.replace(' ', '+')}" - building_address_display = f'=HYPERLINK("{maps_url}","{building_address}")' - - # Create recorded location hyperlink if it's a GPS location - recorded_location_display = recorded_location - if recorded_location and "GPS Location" not in recorded_location and "," in recorded_location: - maps_url = f"https://www.google.com/maps/place/{recorded_location.replace(' ', '+')}" - recorded_location_display = f'=HYPERLINK("{maps_url}","{recorded_location}")' - - # Determine if this is the last pair of the day (for daily total) - is_last_pair = (pair_index == total_pairs_for_day - 1) # Show daily total on second+ pairs - daily_total_display = daily_total_hours if is_last_pair else "" - - row_data = [ - day_name, # A - Day - date_str, # B - Date - check_in_time, # C - In - check_out_time, # D - Out - location, # E - Location - "", # F - Zone (empty) - pair_hours, # G - Hours/Building - daily_total_display, # H - Daily Total (only on last pair) - "", # I - Regular Hours (only on last pair) - "", # J - OT Hours (only on last pair) - building_address_display, # K - Building Address - recorded_location_display, # L - Recorded Location - distance_value, # M - Distance - possible_violation # N - Possible Violation - ] - - # Write the row - for col, value in enumerate(row_data, 1): - cell = worksheet.cell(row=current_row, column=col, value=value) - self._apply_style(cell, self.template_data_style) - - return current_row + 1 - - def _write_single_record_row(self, worksheet, current_row: int, date_obj: datetime, - record, location: str, building_address: str, - total_hours: float, regular_hours: float, ot_hours: float) -> int: - """Write a single record row when there's only one check-in""" - day_name = date_obj.strftime('%A').upper() - date_str = date_obj.strftime('%m/%d/%Y') - - # Single record - estimate check-out time - if record: - check_in_time = record.check_in_time.strftime('%I:%M:%S %p') - # Estimate check-out based on total hours - check_in_datetime = datetime.combine(record.check_in_date, record.check_in_time) - estimated_checkout = check_in_datetime + timedelta(hours=total_hours) - check_out_time = estimated_checkout.strftime('%I:%M:%S %p') - else: - check_in_time = '' - check_out_time = '' - - # Get location accuracy info (same logic as pair row) - location_accuracy = None - recorded_location = '' - distance_value = '' - possible_violation = 'No' - - if record and hasattr(record, 'location_accuracy'): - location_accuracy = getattr(record, 'location_accuracy', None) - if location_accuracy is not None: - try: - accuracy_value = float(location_accuracy) - distance_value = f"{accuracy_value:.3f}" - - if accuracy_value < 0.3: - recorded_location = building_address - possible_violation = "No" - else: - recorded_location = f"GPS Location (Accuracy: {distance_value} miles)" - possible_violation = "Yes" if accuracy_value > 0.5 else "Possible" - except (ValueError, TypeError): - distance_value = "Unknown" - recorded_location = "GPS Location (Unknown Accuracy)" - possible_violation = "Possible" - - row_data = [ - day_name, # A - Day - date_str, # B - Date - check_in_time, # C - In - check_out_time, # D - Out - location, # E - Location - "", # F - Zone - total_hours, # G - Hours/Building - total_hours, # H - Daily Total - "", # I - Regular Hours - "", # J - OT Hours - building_address, # K - Building Address - recorded_location, # L - Recorded Location - distance_value, # M - Distance - possible_violation # N - Possible Violation - ] - - # Write the row - for col, value in enumerate(row_data, 1): - cell = worksheet.cell(row=current_row, column=col, value=value) - self._apply_style(cell, self.template_data_style) - - return current_row + 1 - - def _write_weekly_total_row(self, worksheet, current_row: int, - weekly_regular: float, weekly_ot: float, weekly_total: float) -> int: - """Write weekly total row matching template format""" - - # Weekly Total row (columns G-J) - cell = worksheet.cell(row=current_row, column=7, value="Weekly Total: ") # G - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - cell = worksheet.cell(row=current_row, column=8, value=round(weekly_total, 2)) # H - Daily Total - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - cell = worksheet.cell(row=current_row, column=9, value=round(weekly_regular, 2)) # I - Regular Hours - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - cell = worksheet.cell(row=current_row, column=10, value=round(weekly_ot, 2)) # J - OT Hours - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - # Log weekly total - print(f"โœ… Template export: Weekly total written - Regular: {weekly_regular:.2f}, OT: {weekly_ot:.2f}, Total: {weekly_total:.2f}") - - return current_row + 1 - - def _write_grand_total_row(self, worksheet, current_row: int, - grand_regular: float, grand_ot: float) -> int: - """Write grand total row matching template format""" - - # Grand Total row (columns G, I, J) - cell = worksheet.cell(row=current_row, column=7, value="GRAND TOTAL: ") # G - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - cell = worksheet.cell(row=current_row, column=9, value=round(grand_regular, 2)) # I - Regular Hours - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - cell = worksheet.cell(row=current_row, column=10, value=round(grand_ot, 2)) # J - OT Hours - self._apply_style(cell, self.template_data_style) - cell.font = Font(name="Arial", size=10, bold=True) - - # Log grand total - print(f"โœ… Template export: Grand total written - Regular: {grand_regular:.2f}, OT: {grand_ot:.2f}") - - return current_row + 1 \ No newline at end of file diff --git a/routes/payroll.py b/routes/payroll.py deleted file mode 100644 index 3e4817b..0000000 --- a/routes/payroll.py +++ /dev/null @@ -1,655 +0,0 @@ -""" -routes/payroll.py -================= -Payroll dashboard and Excel export routes. - -Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate, - /api/employee//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//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 \ No newline at end of file diff --git a/templates/base_authenticated.html b/templates/base_authenticated.html index 7dc18ca..b5f8015 100644 --- a/templates/base_authenticated.html +++ b/templates/base_authenticated.html @@ -78,10 +78,6 @@ Time Attendance - - - Payroll - @@ -111,12 +107,7 @@ Time Attendance - + {% elif session.role in ['project_manager'] %} diff --git a/templates/payroll_dashboard.html b/templates/payroll_dashboard.html deleted file mode 100644 index ed3bd20..0000000 --- a/templates/payroll_dashboard.html +++ /dev/null @@ -1,1188 +0,0 @@ -{% extends "base_authenticated.html" %} -{% block title %}Payroll Dashboard - Working Hours{% endblock %} - -{% block extra_head %} - -{% endblock %} - -{% block content %} -
- - - - Back to Dashboard - - - -
-

- - Payroll Dashboard -

-

Calculate employee working hours and generate payroll reports

-
- - -
-

- - Calculation Parameters -

- -
-
- - -
- -
- - -
- -
- - -
- -
- -
-
-
- - - {% if working_hours_data %} -
-

- - Working Hours Summary -

- - -
-
-

{{ working_hours_data.employee_count }}

-

Total Employees

-
-
-

{{ "%.1f"|format(working_hours_data.employees.values() | map(attribute='grand_totals.total_hours') | sum) }}

-

Total Hours

-
-
-

{{ "%.1f"|format(working_hours_data.employees.values() | map(attribute='grand_totals.regular_hours') | sum) }}

-

Regular Hours

-
-
-

{{ "%.1f"|format(working_hours_data.employees.values() | map(attribute='grand_totals.overtime_hours') | sum) }}

-

Overtime Hours

-
-
-

{{ "%.1f"|format(working_hours_data.employees.values() | map(attribute='grand_totals.sp_hours') | sum) }}

-

SP Hours

-
-
-

{{ "%.1f"|format(working_hours_data.employees.values() | map(attribute='grand_totals.pw_hours') | sum) }}

-

PW Hours

-
-
- - -
- - - - - - - - - - - - - - - - {% for employee_id, emp_data in working_hours_data.employees.items() %} - - - - - - - - - - - - {% endfor %} - -
Employee IDEmployee NameTotal HoursRegular HoursOvertime HoursSP HoursPW HoursWorking DaysStatus
{{ employee_id }}{{ employee_names.get(employee_id, 'Employee ' + employee_id) }} - - {{ "%.2f"|format(emp_data.grand_totals.total_hours) }} hrs - - - - {{ "%.2f"|format(emp_data.grand_totals.regular_hours) }} hrs - - - {% if emp_data.grand_totals.overtime_hours > 0 %} - - {{ "%.2f"|format(emp_data.grand_totals.overtime_hours) }} hrs - - {% else %} - - 0.00 hrs - - {% endif %} - - {% if emp_data.grand_totals.sp_hours > 0 %} - - {{ "%.2f"|format(emp_data.grand_totals.sp_hours) }} hrs - - {% else %} - - 0.00 hrs - - {% endif %} - - {% if emp_data.grand_totals.pw_hours > 0 %} - - {{ "%.2f"|format(emp_data.grand_totals.pw_hours) }} hrs - - {% else %} - - 0.00 hrs - - {% endif %} - - {% set working_days = emp_data.daily_hours.values() | selectattr('total_hours', '>', 0) | list | length %} - {{ working_days }} days - - {% set miss_punches = emp_data.daily_hours.values() | selectattr('is_miss_punch', 'equalto', true) | list | length %} - {% if miss_punches > 0 %} - - {{ miss_punches }} miss punch(es) - - {% else %} - - Complete - - {% endif %} -
-
- - -
- -
- - - - - - -
- -
- -
- Report Details:
- Period: {{ working_hours_data.period_start }} to {{ working_hours_data.period_end }}
- Generated: {{ working_hours_data.calculation_date }} -
-
- {% else %} -
-
-
- {% if date_from and date_to %} - No attendance records found for the selected date range.
- Please try a different date range or check if attendance data has been recorded. - {% else %} - Please select a date range to calculate working hours. - {% endif %} -
-
- {% endif %} -
- - - - - -{% endblock %} \ No newline at end of file