import json import smtplib import os import datetime import requests import pandas as pd from email.message import EmailMessage from openpyxl import load_workbook from openpyxl.utils import get_column_letter from openpyxl.styles import Font, Alignment, PatternFill, Border, Side # --- Config and color map --- color_map = { "all_ok": "4CAF50", #Green present "act4_true": "009688", # teal Regularization is accepted "act1_nullOut": "FFC107", #Amber Check out pending "act2": "2196F3", # Bule Regularization pending "act5": "B71C1C", #Dark Red Regularization rejected "all_null": "F6635C", #Red Absent "sunday":"FF0000" } # Add Legend sheet to the workbook legend_data = { "Color Description": [ "Present (All Ok)", "Regularization Accepted", "Check-out Pending", "Regularization Pending", "Regularization Rejected", "Absent", "Sundays" ], "Hex Color": [ color_map["all_ok"], color_map["act4_true"], color_map["act1_nullOut"], color_map["act2"], color_map["act5"], color_map["all_null"], color_map["sunday"] ] } def login_api(): payload = {"username": API_USERNAME, "password": API_PASSWORD} headers = {"Content-Type": "application/json"} try: response = requests.post(f"{BASE_URL}/auth/login", json=payload, headers=headers) response.raise_for_status() data = response.json()["data"] jwt = data["token"] print("API login successful.") return jwt except Exception as e: print(f"Login API error: {e}") return None def select_tenant(jwt): headers = {"Authorization": f"Bearer {jwt}", "Content-Type": "application/json"} try: response = requests.post(f"{BASE_URL}/auth/select-tenant/{API_TENANT}", headers=headers) response.raise_for_status() data = response.json()["data"] jwt = data["token"] print("Tenant selected successful.") return jwt except Exception as e: print(f"Select tenant error: {e}") return None def attendance_report(jwt,is_current_month): headers = {"Authorization": f"Bearer {jwt}", "Content-Type": "application/json"} response = requests.get(f"{BASE_URL}/report/report-attendance?isCurrentMonth={is_current_month}", headers=headers) response.raise_for_status() data = response.json() projects = data.get('data', []) # Get current date and time today = datetime.datetime.now() if not is_current_month: first_day_current_month = today.replace(day=1) last_month_last_day = first_day_current_month - datetime.timedelta(days=1) month_name = last_month_last_day.strftime("%B") year = last_month_last_day.year else: month_name = today.strftime("%B") year = today.year excel_file = f"{month_name}-{year}_attendance_report.xlsx" writer = pd.ExcelWriter(excel_file, engine="openpyxl") # Border style thin = Side(border_style="thin", color="000000") border = Border(left=thin, right=thin, top=thin, bottom=thin) for proj_idx, project in enumerate(projects): project_name = project.get('projectName', 'UnknownProject')[:31] # Sheet name max 31 chars attendances = project.get('projectAttendance', []) # Collect all unique dates in this project all_dates = set() for user in attendances: for att in user.get('attendances', []): all_dates.add(att.get('attendanceDate', '').split('T')[0]) all_dates = sorted(all_dates) # Prepare rows rows = [] for user in attendances: row = {'Name': f"{user.get('firstName','')} {user.get('lastName','')}".strip()} date_map = {a.get('attendanceDate', '').split('T')[0]: a for a in user.get('attendances', [])} for date in all_dates: att = date_map.get(date, {}) # Store also needed fields for coloring below row[f"{date}_checkin"] = '' row[f"{date}_checkout"] = '' if att.get('checkIn'): # Extract time part only, e.g. "09:30:00" row[f"{date}_checkin"] = att.get('checkIn').split('T')[1] if att.get('checkOut'): # Extract time part only row[f"{date}_checkout"] = att.get('checkOut').split('T')[1] row[f"{date}_activity"] = att.get('activity', None) row[f"{date}_isapproved"] = att.get('isApproved', None) row['CheckInCheckOutDone'] = user.get('checkInCheckOutDone', 0) row['CheckOutPending'] = user.get('checkOutPending', 0) row['CheckInDone'] = user.get('checkInDone', 0) row['AbsentAttendance'] = user.get('absentAttendance', 0) row['RejectedRegularize'] = user.get('rejectedRegularize', 0) rows.append(row) legend_df = pd.DataFrame(legend_data) # Use the same writer to add sheet after all sheets written legend_df.to_excel(writer, sheet_name="Legend", index=False) # Access Legend sheet for coloring the color sample cells ws_legend = writer.book["Legend"] # Assuming data starts from row 2 (header in row 1) for row_idx in range(2, 2 + len(legend_data["Color Description"])): # Color sample cell: B column (2nd col) cell = ws_legend.cell(row=row_idx, column=2) hex_color = cell.value fill = PatternFill(start_color=hex_color, fill_type="solid") cell.fill = fill # Optional: bold and center description column desc_cell = ws_legend.cell(row=row_idx, column=1) desc_cell.font = Font(bold=True) desc_cell.alignment = Alignment(horizontal="left", vertical="center") # Optionally adjust headers and column width for visual clarity for col in [1, 2]: ws_legend.column_dimensions[get_column_letter(col)].width = 30 ws_legend["A1"].font = Font(bold=True) ws_legend["B1"].font = Font(bold=True) ws_legend["A1"].alignment = ws_legend["B1"].alignment = Alignment(horizontal="center") # Columns: Name, checkin/checkout pairs for dates, and summary columns columns = ['Name'] for date in all_dates: columns.extend([f"{date}_checkin", f"{date}_checkout"]) columns.extend(['CheckInCheckOutDone', 'CheckInDone','CheckOutPending', 'AbsentAttendance', 'RejectedRegularize']) df = pd.DataFrame(rows, columns=columns) # Write to sheet df.to_excel(writer, sheet_name=project_name, index=False, startrow=2) # Post-process sheet using openpyxl wb = writer.book ws = wb[project_name] # Merged header with date and subheaders ws["A3"].value = "Name" ws["A3"].font = Font(bold=True) ws["A3"].alignment = Alignment(horizontal="center", vertical="center") col = 2 for date in all_dates: col_letter_1 = get_column_letter(col) col_letter_2 = get_column_letter(col + 1) ws.merge_cells(f"{col_letter_1}2:{col_letter_2}2") ws[f"{col_letter_1}2"].value = pd.to_datetime(date).strftime("%d-%m-%Y") ws[f"{col_letter_1}2"].font = Font(bold=True) ws[f"{col_letter_1}2"].alignment = Alignment(horizontal="center", vertical="center") ws[f"{col_letter_1}3"].value = "Check-in" ws[f"{col_letter_2}3"].value = "Check-out" ws[f"{col_letter_1}3"].alignment = ws[f"{col_letter_2}3"].alignment = Alignment(horizontal="center") col += 2 # Add headers for summary columns summary_headers = ['CheckIn-CheckOut Done', 'CheckIn Done', 'CheckOut Pending', 'Absent Attendance', 'Rejected Regularize'] summary_start_col = col for i, header in enumerate(summary_headers): col_letter = get_column_letter(summary_start_col + i) ws.merge_cells(f"{col_letter}2:{col_letter}3") ws[f"{col_letter}2"].value = header ws[f"{col_letter}2"].font = Font(bold=True) ws[f"{col_letter}2"].alignment = Alignment(horizontal="center", vertical="center") # Apply borders and coloring max_row = ws.max_row max_col = ws.max_column for row_idx in range(3, max_row + 1): # Starting from 3 to include header rows for col_idx in range(1, max_col + 1): cell = ws.cell(row=row_idx, column=col_idx) cell.border = border # Coloring according to rules for check-in/out pairs for row_idx, user_row in enumerate(rows, start=4): for date_idx, date in enumerate(all_dates): checkin_col = 2 + date_idx * 2 checkout_col = checkin_col + 1 activity = user_row.get(f"{date}_activity") isapproved = user_row.get(f"{date}_isapproved") c_in = user_row.get(f"{date}_checkin") c_out = user_row.get(f"{date}_checkout") # --- Sunday RED coloring --- dt = datetime.datetime.strptime(date, "%Y-%m-%d") if dt.weekday() == 6: # Sunday fill = PatternFill(start_color="FF0000", fill_type="solid") ws.cell(row=row_idx, column=checkin_col).fill = fill ws.cell(row=row_idx, column=checkout_col).fill = fill continue # Skip further coloring for this date fill_color = None if not c_in and not c_out: fill_color = color_map["all_null"] elif c_in and c_out and activity == 4 and not bool(isapproved): fill_color = color_map["all_ok"] elif activity == 4 and bool(isapproved): fill_color = color_map["act4_true"] elif activity == 1 and not c_out: fill_color = color_map["act1_nullOut"] elif activity == 2: fill_color = color_map["act2"] elif activity == 5: fill_color = color_map["act5"] if fill_color: fill = PatternFill(start_color=fill_color, fill_type="solid") ws.cell(row=row_idx, column=checkin_col).fill = fill ws.cell(row=row_idx, column=checkout_col).fill = fill writer.close() print(f"Excel '{excel_file}' generated with sheets per project, summary columns and conditional coloring.") # --- 2. Compose the Email ---- msg = EmailMessage() msg["Subject"] = f"Attendance Report for {month_name}-{year}" msg["From"] = SENDER_EMAIL msg["To"] = RECIPIENT_EMAILS msg.set_content("Please find the attached Excel file.") # Add the Excel attachment with open(excel_file, "rb") as f: file_data = f.read() file_name = f.name msg.add_attachment(file_data, maintype="application", subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=file_name) # --- 3. Send the Email ---- with smtplib.SMTP(SMPTSERVER, PORT) as smtp: smtp.ehlo() smtp.starttls() smtp.ehlo() smtp.login(SENDER_EMAIL, SENDER_PASSWORD) smtp.send_message(msg) # After all operations (e.g., emailing) are done, delete the file if os.path.exists(excel_file): os.remove(excel_file) print(f"Deleted file: {excel_file}") else: print("File not found, cannot delete.") # --- Main execution logic --- if __name__ == "__main__": import sys # Load your real config before calling any API GLOBAL_CONFIG_PATH = "config.json" try: with open(GLOBAL_CONFIG_PATH, "r", encoding="utf-8") as f: config = json.load(f) except Exception as e: print(f"Failed to load config: {e}") sys.exit(1) API_CONFIG = config.get("API", {}) BASE_URL = API_CONFIG.get("BASE_URL") API_USERNAME = API_CONFIG.get("USERNAME") API_PASSWORD = API_CONFIG.get("PASSWORD") API_TENANT = API_CONFIG.get("TENANTID") SMPT_CONFIG = config.get("SMPT", {}) SMPTSERVER = SMPT_CONFIG.get("SMPTSERVER") PORT = SMPT_CONFIG.get("PORT") SENDER_EMAIL = SMPT_CONFIG.get("SENDER_EMAIL") SENDER_PASSWORD = SMPT_CONFIG.get("SENDER_PASSWORD") RECIPIENT_EMAILS = SMPT_CONFIG.get("RECIPIENT_EMAILS") token = login_api() if not token: print("Login failed, aborting.") sys.exit(1) jwt_token = select_tenant(token) if not jwt_token: print("Tenant selection failed, aborting.") sys.exit(1) attendance_report(jwt_token, False)