""" 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