【python】月报考勤工时计算

根据打卡时间,计算每日工时,中间存在午餐午休、晚餐时间,只有当工时区间覆盖了午休,才扣午休;覆盖多少,扣多少。

出勤工时
= 取整后下班时间 − 取整后上班时间
− 实际覆盖的午休时间
− 实际覆盖的晚餐时间

py 复制代码
import os
import traceback
import logging
import numpy as np
import pandas as pd
import tkinter as tk
from tkinter import filedialog, messagebox


# =========================
# 配置区
# =========================
DEVICE_KEYWORD = "产业城总部_"
OUT_SUMMARY_NAME = "考勤月报_横向工时.xlsx"
OUT_DETAIL_NAME = "考勤工时_计算明细.xlsx"
LOG_FILE = "attendance.log"


# =========================
# 时间 / 区间工具函数
# =========================

def parse_datetime(date_str, time_str):
    """日期 + 时间 -> Timestamp"""
    return pd.to_datetime(f"{date_str} {time_str}", errors="coerce")


def round_to_half_hour(ts):
    """15 分钟阈值,取整到最近半小时"""
    if pd.isna(ts):
        return ts
    minutes = ts.hour * 60 + ts.minute
    rounded = ((minutes + 15) // 30) * 30
    return ts.normalize() + pd.Timedelta(minutes=int(rounded))

# 只有当工时区间覆盖了午休,才扣午休;覆盖多少,扣多少
def overlap_minutes(a_start, a_end, b_start, b_end):
    """计算两个时间区间的重叠分钟数"""
    start = max(a_start, b_start)
    end = min(a_end, b_end)
    if end <= start:
        return 0
    return int((end - start).total_seconds() // 60)


# =========================
# 单日明细计算
# =========================

def compute_daily_detail(group):
    """
    计算某员工某一天的考勤明细
    """
    date_str = group["日期"].iloc[0]
    day = pd.to_datetime(date_str).normalize()

    times = [parse_datetime(date_str, t) for t in group["打卡时间"]]
    times = [t for t in times if not pd.isna(t)]

    if not times:
        return {
            "出勤工时": np.nan,
            "上班时间(用于计算)": None,
            "下班时间(用于计算)": None,
            "原始最早打卡": None,
            "原始最晚打卡": None,
            "打卡次数": 0,
        }

    raw_start = min(times)
    raw_end = max(times)

    start_limit = day + pd.Timedelta(hours=8, minutes=30)
    work_start = max(raw_start, start_limit)
    work_end = max(raw_end, work_start)

    work_start_calc = round_to_half_hour(work_start)
    work_end_calc = round_to_half_hour(work_end)

    gross_minutes = int((work_end_calc - work_start_calc).total_seconds() // 60)

    lunch = overlap_minutes(
        work_start_calc, work_end_calc,
        day + pd.Timedelta(hours=12),
        day + pd.Timedelta(hours=13, minutes=30)
    )

    dinner = overlap_minutes(
        work_start_calc, work_end_calc,
        day + pd.Timedelta(hours=17, minutes=30),
        day + pd.Timedelta(hours=18, minutes=30)
    )

    net_minutes = max(0, gross_minutes - lunch - dinner)

    return {
        "出勤工时": net_minutes / 60,
        "上班时间(用于计算)": work_start_calc.strftime("%H:%M"),
        "下班时间(用于计算)": work_end_calc.strftime("%H:%M"),
        "原始最早打卡": raw_start.strftime("%H:%M:%S"),
        "原始最晚打卡": raw_end.strftime("%H:%M:%S"),
        "打卡次数": len(times),
    }


# =========================
# 核心处理逻辑(输出到指定目录)
# =========================

def process_attendance(input_file, out_dir):
    """
    生成:
    1) 横向月报
    2) 纵向计算明细
    """
    logging.info(f"读取源文件: {input_file}")

    df = pd.read_excel(input_file, dtype=str)
    df = df[df["设备名称"].astype(str).str.contains(DEVICE_KEYWORD, na=False)]

    if df.empty:
        raise ValueError(f"未找到设备名称包含"{DEVICE_KEYWORD}"的记录")

    summary_rows = []
    detail_rows = []

    for (emp_id, name, date_str), g in df.groupby(["工号", "姓名", "日期"]):
        detail = compute_daily_detail(g)
        date_dt = pd.to_datetime(date_str)

        detail_rows.append({
            "工号": emp_id,
            "姓名": name,
            "日期": date_dt,
            **detail
        })

        summary_rows.append({
            "工号": emp_id,
            "姓名": name,
            "日期": date_dt,
            "日期列": date_dt.strftime("%m-%d"),
            "出勤工时": detail["出勤工时"],
        })

    # ===== 明细表 =====
    detail_df = pd.DataFrame(detail_rows)
    detail_df = detail_df.sort_values(["工号", "日期"])
    detail_df["日期"] = detail_df["日期"].dt.strftime("%Y-%m-%d")

    detail_path = os.path.join(out_dir, OUT_DETAIL_NAME)
    detail_df.to_excel(detail_path, index=False)

    # ===== 月报 =====
    summary_df = pd.DataFrame(summary_rows)

    date_columns = (
        summary_df.sort_values("日期")["日期列"]
        .drop_duplicates()
        .tolist()
    )

    summary_table = summary_df.pivot_table(
        index=["工号", "姓名"],
        columns="日期列",
        values="出勤工时",
        aggfunc="first"
    ).reset_index()

    summary_table = summary_table.reindex(
        columns=["工号", "姓名"] + date_columns
    )

    summary_path = os.path.join(out_dir, OUT_SUMMARY_NAME)
    summary_table.to_excel(summary_path, index=False)

    logging.info("生成完成")
    return summary_path, detail_path


# =========================
# GUI 主入口
# =========================

def main():
    logging.basicConfig(
        filename=LOG_FILE,
        level=logging.INFO,
        format="%(asctime)s - %(levelname)s - %(message)s",
        encoding="utf-8"
    )

    try:
        root = tk.Tk()
        root.title("考勤工时计算")
        root.geometry("360x160")

        tk.Label(root, text="请选择打卡源数据 Excel").pack(pady=8)

        def run():
            try:
                # ① 选源文件
                input_file = filedialog.askopenfilename(
                    parent=root,
                    title="请选择打卡源数据 Excel",
                    filetypes=[("Excel 文件", "*.xlsx *.xls")]
                )
                if not input_file:
                    return

                # ② 选输出目录
                out_dir = filedialog.askdirectory(
                    parent=root,
                    title="请选择结果保存目录"
                )
                if not out_dir:
                    return

                summary_path, detail_path = process_attendance(input_file, out_dir)

                messagebox.showinfo(
                    "完成",
                    "已生成文件:\n\n"
                    f"{summary_path}\n"
                    f"{detail_path}"
                )

            except Exception:
                logging.exception("处理失败")
                messagebox.showerror(
                    "错误",
                    f"程序运行失败,请查看 {LOG_FILE}"
                )

        tk.Button(root, text="开始计算", command=run).pack(pady=12)
        root.mainloop()

    except Exception:
        with open("fatal_error.log", "w", encoding="utf-8") as f:
            f.write(traceback.format_exc())


if __name__ == "__main__":
    main()
相关推荐
fl1768312 小时前
基于python实现PDF批量加水印工具
开发语言·python·pdf
i02082 小时前
Prompt
python
Freed&2 小时前
用 Python 写一个“会下小纸条雨”的暖心程序 —— Flask 网页版 + Tkinter 桌面版
python
_codemonster2 小时前
手语识别及翻译项目实战系列(五)整体架构代码详细代码实现
人工智能·python·计算机视觉·架构
Eugene__Chen2 小时前
Java的SPI机制(曼波版)
java·开发语言·python
程序猿20232 小时前
JVM与JAVA
java·jvm·python
cici158742 小时前
基于LSTM算法的MATLAB短期风速预测实现
开发语言·matlab
独隅2 小时前
本地大模型训练与 API 服务部署全栈方案:基于 Ubuntu 22.04 LTS 的端到端实现指南
服务器·python·语言模型
程序员miki2 小时前
训练yolo11检测模型经验流程
python·yolo