Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!

Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!

“Crossing the vast sea of data in office automation”

@MoYu

Introduction

Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!

Hello everyone! I am your new friend MoYu. Over the past ten years of my daily work, I have accumulated many practical skills in Python office automation projects. Starting today, I will share them on the “Code Sea Listening to the Tide” public account! Packed with practical office tips, I hope to discuss ways to improve office efficiency with all of you and achieve the freedom of not working overtime.

Alright, without further ado, let’s get straight to the practical content.

From Collapse to Elegant Evolution

Office Demand Scenario

Scenario Description: There is a mysterious Excel workbook exported from the attendance machine containing the card swipe records of employees from various departments for the month. It includes the normal working hours (08:00-20:00) and overtime hours (after 20:00) of company employees, which will be used to verify and calculate each employee’s daily working hours and overtime hours, and summarize the total working hours and overtime hours for the month.

The required Excel workbook is shown in the figure below:

Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!

Pain Point: The amount of data to verify is large, and if we follow the traditional manual method of checking with a calculator, it would take forever. This is where Python comes to the rescue.

Problem Breakdown:

1. Based on the company’s normal working hours and overtime hours, use Python code to calculate the daily working hours and overtime hours from each card swipe record, and add them as comments in the cells for easy verification.

2. Summarize the daily working hours and overtime hours using Python code and add them to the last two columns of each row of work hour data.

Now, let’s use Python code to show Excel what it means when “traditional culture meets cyber efficiency”.

import re
import xlwings as xw
import os
from datetime import datetime, timedelta

def load_excel():
    app = xw.App(visible=True)
    return app

def get_cell_info(cell):
    if cell.merge_cells:
        merge_area = cell.merge_area
        merge_max_row = merge_area.last_cell.row
        merge_max_col = merge_area.last_cell.column
        value = merge_area[0, 0].value
    else:
        merge_max_row = cell.row
        merge_max_col = cell.column
        value = cell.value
    return merge_max_row, merge_max_col, value

def get_max_row_col(sheet):
    max_col = sheet.used_range.last_cell.column
    max_row = sheet.used_range.last_cell.row
    return max_row, max_col

def custom_region(sheet, start_cell, up_search_text=None, down_search_text=None, left_search_text=None, right_search_text=None):
    start_range = sheet.range(start_cell)
    first_row = start_range.row
    first_col = start_range.column
    # (1) Find the last row downwards
    last_row = first_row
    while last_row <= sheet.used_range.last_cell.row:
        cell_value = sheet.cells(last_row + 1, first_col).value
        if cell_value is None:
            break
        if down_search_text is not None and str(cell_value) == down_search_text:
            break
        last_row += 1
    # (2) Find the first row upwards
    first_row_temp = first_row
    while first_row_temp > 1:
        cell_value = sheet.cells(first_row_temp - 1, first_col).value
        if cell_value is None:
            break
        if up_search_text is not None and not re.search(up_search_text, str(cell_value)):
            break
        first_row_temp -= 1
    # (3) Find the last column to the right
    last_col = first_col
    while last_col <= sheet.used_range.last_cell.column:
        cell_value = sheet.cells(first_row, last_col + 1).value
        if cell_value is None:
            break
        last_col += 1
    # (4) Find the first column to the left
    first_col_temp = first_col
    while first_col_temp > 1:
        cell_value = sheet.cells(first_row, first_col_temp - 1).value
        if cell_value is None:
            break
        first_col_temp -= 1
    return sheet.range((first_row_temp, first_col_temp), (last_row, last_col))

def calculate_work_hours(start_time_str, end_time_str):
    start_time = datetime.strptime(start_time_str, "%H:%M")
    end_time = datetime.strptime(end_time_str, "%H:%M")
    work_start = datetime.strptime("08:00", "%H:%M")
    work_end = datetime.strptime("20:00", "%H:%M")
    if end_time < start_time:
        end_time += timedelta(days=1)
        work_end += timedelta(days=1)
    normal_start = max(start_time, work_start)
    normal_end = min(end_time, work_end)
    total_duration = max(normal_end - normal_start, timedelta(0))
    normal_duration = min(total_duration, work_end - work_start)
    overtime_duration = max(end_time - max(work_end, start_time), timedelta(0))
    def timedelta_to_hours(td):
        return round(td.total_seconds() / 3600, 2)
    normal_hours = timedelta_to_hours(normal_duration)
    overtime_hours = timedelta_to_hours(overtime_duration)
    return {"Working Hours": f"{normal_hours}", "Overtime Hours": f"{overtime_hours}"}

search_text = 'Name'
file_path = r'G:\Desktop\9999999\Attendance_Statistics.xlsx'
# Start Excel application
app = load_excel()
wb = app.books.open(os.path.abspath(file_path))
found_in_sheet = False

for sheet in wb.sheets:
    if not sheet.visible:
        continue
    found = sheet.used_range.api.Find(What=search_text,
                                       LookIn=xw.constants.FindLookIn.xlValues,
                                       LookAt=xw.constants.LookAt.xlPart,
                                       SearchOrder=xw.constants.SearchOrder.xlByRows,
                                       SearchDirection=xw.constants.SearchDirection.xlNext,
                                       MatchCase=False)
    if found is None:
        continue
    first_address = found.Address
    while found is not None:
        found_in_sheet = True
        # Get the xlwings Range object of the found cell
        found_cell = sheet.range(found.Address)
        if found_cell.merge_cells:
            merge_area = found_cell.merge_area
            merge_min_row = merge_area.row
            merge_max_row = merge_area.last_cell.row
            merge_min_col = merge_area.column
            merge_max_col = merge_area.last_cell.column
        else:
            merge_min_row = found_cell.row
            merge_max_row = found_cell.row
            merge_min_col = found_cell.column
            merge_max_col = found_cell.column
        start_row = merge_min_row + 1
        end_row = merge_max_row + 1
        start_col = merge_min_col
        end_col = merge_max_col + 2
        start_cell = sheet.cells(end_row, end_col).address
        data_range = custom_region(
            sheet,
            start_cell,
            up_search_text=r'\d+',
            down_search_text='Swipe Record Table',
            left_search_text=None,
            right_search_text=None
        )
        data_range.number_format = '@'
        # Add titles
        sheet.cells(data_range.row, data_range.columns.count + 1).value = 'Working Hours (h)'
        sheet.cells(data_range.row, data_range.columns.count + 2).value = 'Overtime Hours (h)'
        # Set format
        for col_offset in [data_range.columns.count + 1, data_range.columns.count + 2]:
            cell = sheet.cells(data_range.row, col_offset)
            cell.font.size = 10
            cell.api.Borders.LineStyle = 1  # xlContinuous
            cell.api.Borders.Weight = 2  # xlThin
            cell.color = (198, 224, 180)  # Light green
        # Process data
        for row_idx in range(data_range.row, data_range.row + data_range.rows.count):
            ls = []
            lt = []
            for col_idx in range(data_range.column, data_range.column + data_range.columns.count):
                cell = sheet.cells(row_idx, col_idx)
                if cell.value is not None and isinstance(cell.value, str):
                    if re.search(r'(\d{2}:\d{2})', cell.value):
                        times = re.findall(r'(\d{2}:\d{2})', cell.value)
                        times.sort()
                        start_time_str, end_time_str = times[0], times[-1]
                        dic = calculate_work_hours(start_time_str, end_time_str)
                        ls.append(dic['Working Hours'])
                        lt.append(dic['Overtime Hours'])
                        # Add comment
                        if cell.api.Comment is not None:  # Check if comment exists
                            cell.api.Comment.Delete()  # Delete existing comment
                        str_comment = "\n".join(f"{k}: {v} hours" for k, v in dic.items())
                        cell.api.AddComment(str_comment)  # Add new comment
            if ls:
                sum_worktime = sum(map(float, ls))
                sum_overtime = sum(map(float, lt))
                work_cell = sheet.cells(row_idx, data_range.column + data_range.columns.count)
                overtime_cell = sheet.cells(row_idx, data_range.column + data_range.columns.count + 1)
                work_cell.value = sum_worktime
                overtime_cell.value = sum_overtime
                for cell in [work_cell, overtime_cell]:
                    cell.font.size = 10
                    cell.api.Borders.LineStyle = 1  # xlContinuous
                    cell.api.Borders.Weight = 2  # xlThin
                    cell.color = (198, 224, 180)  # Light green
        # Find the next match
        found = sheet.used_range.api.FindNext(found)
        if found is None or found.Address == first_address:
            break
# Save and close
wb.save()
app.quit()

The final saved result Excel workbook is shown in the figure below:

Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!

With the above Python automation script, the task that originally required hours of manual calculation can be completed in just 4 minutes. From the initial despair of manual operation to the efficient automation achieved with Python, work efficiency has increased exponentially, finally achieving the freedom of not working overtime! Everyone can draw parallels and adapt the code logic above to analyze specific problems based on their actual work situations. Additionally, the exe program has been packaged; if you need the packaged exe without a Python environment, feel free to ask MoYu. The exe program is shown in the figure below:

Stop Manually Checking Attendance! Use Python to Process Attendance Machine Data and Batch Generate Work Hours!

Leave a Comment