请假没批就让人找代班?请假单审批状态过滤版核对脚本

2026-10-08 · 喵窝资源铺

上一篇《值班表×请假单跨表核对》有个没说破的漏洞:它把请假单里所有行都当真——「审批中」的假、「驳回」的假、「已销假」的假全拿去撞值班表。后果是:员工随手提了个请假申请还没批,行政就被脚本提醒「10-10 夜班要找人代班」,白忙一场。这版加上审批状态过滤:只有「已通过」的请假参与冲突判定,其余状态单独进一张表存着。代码实跑验证,真实输出贴在下面。

🎁 免费模板:下载请假状态过滤核对模板(值班表+请假单+说明页,示例行可直接跑通——3 处冲突、「审批中/空白/已销假」3 条全被正确拦下、1 行脏行按预期归档,删掉示例换成你的数据即可)。

输出长什么样

构造测试数据:值班表 12 行(1 行脏行,有效 11 条),请假单 9 条——故意埋了 4 个状态陷阱:王五「审批中」撞了夜班(不过滤就会误报)、钱七状态列空白(最危险:默认放行就错)、孙八「已销假」(假后来不请了)、小五「待领导批复」(白名单/黑名单都没收录的模糊词)。另有 1 条起止填反的(周十)。跑完:

值班 11 条 / 请假有效 5 条 / 未通过或待定 4 条 / 冲突 3 处 / 漏排 0 / 脏行 1
冲突: 李四 请假2026-10-03~2026-10-05(事假) 撞值班日 2026-10-04, 2026-10-05 班次[夜班、白班] → 需代班
冲突: 赵六 请假2026-10-12~2026-10-12(调休) 撞值班日 2026-10-12 班次[夜班] → 需代班
冲突: 周十 请假2026-10-15~2026-10-17(事假) 撞值班日 2026-10-15, 2026-10-16 班次[夜班] → 需代班
未参与: 王五 请假2026-10-10~2026-10-10 状态[不通过] → 不参与冲突判定
未参与: 钱七 请假2026-10-08~2026-10-08 状态[空白] → 不参与冲突判定
未参与: 孙八 请假2026-10-06~2026-10-06 状态[不通过] → 不参与冲突判定
未参与: 小五 请假2026-10-11~2026-10-11 状态[未知] → 不参与冲突判定

逐项验收:王五「审批中」没被报成冲突(这是和上一版最大的差别);钱七的空白状态不默认放行而是进未参与表等人确认;孙八「已销假」正确拦下;小五「待领导批复」被模糊兜底归为未知→拦下;周十起止填反(17 号在前)被自动交换后正常撞出 15、16 两天;张三年假已通过但值班只到 13 号,不误报。幂等验证:连跑两次输出 cmp 完全一致。

状态词怎么归一化

现实里的请假单状态列一万个写法。脚本分三层:

层规则例子
精确匹配完整词表白名单/黑名单(忽略大小写和首尾空格)已通过/通过/同意/已批准/批准/approved → 通过;审批中/驳回/已销假/撤销/退回 → 不通过
模糊兜底包含「通过/同意/批准/生效」且不含「未/驳回/拒绝」→ 通过;包含「驳回/拒绝/销假/撤销/取消」→ 不通过「待领导批复」都不含 → 未知
兜不住的空白或未知 → 一律不参与,进「未审批或被驳回」表等人确认状态忘填的那行

原则就一条:宁可漏报一条没批的假,不能拿没批的假让行政白找代班。漏的那条会躺在未参与表里,人一眼能看到;误报的代班找下去才发现假根本没批,这个信任就没了。

用法

先 pip install openpyxl。值班表列:日期 / 班次 / 姓名;请假单列:姓名 / 开始日期 / 结束日期 / 状态(状态列是这版的必需列,缺了直接报错——如果你家请假单没有状态列,用基础版)。运行:

python leave_status_check.py 值班表.xlsx 请假单.xlsx 核对结果.xlsx

输出 xlsx 四张表:冲突明细(已通过的假撞了班)、未审批或被驳回(不参与判定,待确认)、请假但无排班(请了假但值班表没这人)、跳过脏行。

完整脚本

# -*- coding: utf-8 -*-
"""请假单审批状态过滤版:只认「已通过」的请假去撞值班表。
审批中/被驳回/已销假的请假不参与冲突判定(避免拿没批的假让行政白找代班)。
用法: python leave_status_check.py 值班表.xlsx 请假单.xlsx 输出.xlsx
请假单必需列: 姓名/开始日期/结束日期/状态。状态归一化后按黑/白名单过滤:
  通过类(参与): 已通过/通过/同意/已同意/审批通过/已批准/批准/approved/已生效/生效
  不通过类(不参与但计数): 审批中/待审批/待审核/审核中/驳回/已驳回/拒绝/未通过/已拒绝/rejected
  特殊: 已销假/撤销/已撤销 → 算作不参与
  状态列缺失或空 → 看不出批没批 → 不参与并进「状态待确认」提醒表,绝不默认放行
"""
import sys, datetime as dt
from openpyxl import load_workbook, Workbook

duty_path, leave_path, dst = sys.argv[1], sys.argv[2], sys.argv[3]

APPROVED = {"已通过","通过","同意","已同意","审批通过","已批准","批准","approved","已生效","生效","ok","已同意请假"}
NOT_APPROVED = {"审批中","待审批","待审核","审核中","审批中...","驳回","已驳回","拒绝","未通过","已拒绝","rejected","已销假","销假","撤销","已撤销","cancelled","已取消","待修改","退回"}

def norm_status(v):
    s = str(v).strip().lower() if v is not None else ""
    if not s: return ""
    if s in APPROVED: return "通过"
    if s in NOT_APPROVED: return "不通过"
    # 模糊兜底:包含关键词
    for kw in ("通过","同意","批准","生效","approved"):
        if kw in s or kw in s.lower() or kw in str(v).strip():
            if "未" not in s and "驳回" not in s and "拒绝" not in s:
                return "通过"
    for kw in ("驳回","拒绝","销假","撤销","取消","退回","rejected","cancel"):
        if kw in s or kw in str(v).strip().lower(): return "不通过"
    return "未知"

def norm_date(v):
    if v is None: return None
    if isinstance(v, dt.datetime): return v.date()
    s = str(v).strip()
    for sep in ("-", "/"):
        p = s.split(sep)
        if len(p) == 3:
            try: return dt.date(int(p[0]), int(p[1]), int(p[2]))
            except ValueError: pass
    return None

def find_header(ws, cols, max_row=10):
    for row in ws.iter_rows(min_row=1, max_row=max_row):
        vals = [str(c.value).strip() if c.value is not None else "" for c in row]
        if all(col in vals for col in cols):
            return row[0].row, {col: vals.index(col) for col in cols}
    return None, None

# 值班表
wb = load_workbook(duty_path, data_only=True)
found = [(w, *find_header(w, ["姓名","日期"])) for w in wb.worksheets]
ws, hr, hdr = next((w, r, h) for w, r, h in found if r)
row_vals = [str(c.value).strip() if c.value is not None else "" for c in next(ws.iter_rows(min_row=hr, max_row=hr))]
shift_idx = row_vals.index("班次") if "班次" in row_vals else None
duties, dirty = {}, []
for row in ws.iter_rows(min_row=hr+1):
    name = str(row[hdr["姓名"]].value).strip() if row[hdr["姓名"]].value else ""
    date = norm_date(row[hdr["日期"]].value)
    shift = str(row[shift_idx].value).strip() if shift_idx is not None and row[shift_idx].value else "未知"
    if not name or date is None:
        if any(c.value for c in row): dirty.append(f"值班表第{row[0].row}行: 姓名/日期缺失")
        continue
    duties.setdefault(name, []).append((date, shift))

# 请假单(带状态列)
wb2 = load_workbook(leave_path, data_only=True)
sheets2 = [(w, *find_header(w, ["姓名","开始日期","结束日期","状态"])) for w in wb2.worksheets]
if not any(r for _, r, _ in sheets2):
    sys.exit("错误: 请假单没有找到「姓名/开始日期/结束日期/状态」列。审批过滤版必须有状态列,否则用基础版 duty_leave_check.py")
ws2, hr2, h2 = next((w, r, h) for w, r, h in sheets2 if r)
extra = {}
row_vals2 = [str(c.value).strip() if c.value is not None else "" for c in next(ws2.iter_rows(min_row=hr2, max_row=hr2))]
for col in ("请假类型","事由"):
    if col in row_vals2: extra[col] = row_vals2.index(col)

leaves, pending = [], []
for row in ws2.iter_rows(min_row=hr2+1):
    name = str(row[h2["姓名"]].value).strip() if row[h2["姓名"]].value else ""
    d1, d2 = norm_date(row[h2["开始日期"]].value), norm_date(row[h2["结束日期"]].value)
    st = norm_status(row[h2["状态"]].value)
    if not name or d1 is None or d2 is None:
        if any(c.value for c in row): dirty.append(f"请假单第{row[0].row}行: 姓名/日期缺失")
        continue
    if d2 < d1: d1, d2 = d2, d1  # 起止填反自动纠正并提醒
    typ = str(row[extra["请假类型"]].value).strip() if "请假类型" in extra and row[extra["请假类型"]].value else ""
    reason = str(row[extra["事由"]].value).strip() if "事由" in extra and row[extra["事由"]].value else ""
    if st != "通过":
        pending.append((name, d1, d2, typ, st or "空白", reason))
        continue
    leaves.append((name, d1, d2, typ, reason))

conflicts, no_duty = [], []
for name, d1, d2, typ, reason in leaves:
    hits = [(d, s) for d, s in duties.get(name, []) if d1 <= d <= d2]
    if hits:
        conflicts.append((name, d1, d2, typ, ", ".join(str(d) for d, _ in hits), "、".join(sorted({s for _, s in hits})), reason))
    elif name not in duties:
        no_duty.append((name, d1, d2, typ, reason))

out = Workbook()
w1 = out.active; w1.title = "冲突明细"
w1.append(["姓名","请假开始","请假结束","请假类型","撞到的值班日","需代班班次","事由"])
for c in conflicts: w1.append(list(c))
w2 = out.create_sheet("未审批或被驳回")
w2.append(["姓名","请假开始","请假结束","请假类型","状态","事由"])
for r in pending: w2.append(list(r))
w3 = out.create_sheet("请假但无排班")
w3.append(["姓名","请假开始","请假结束","请假类型","事由"])
for r in no_duty: w3.append(list(r))
w4 = out.create_sheet("跳过脏行")
w4.append(["原因"])
for r in dirty: w4.append([r])
out.save(dst)

print(f"值班 {sum(len(v) for v in duties.values())} 条 / 请假有效 {len(leaves)} 条 / 未通过或待定 {len(pending)} 条 / 冲突 {len(conflicts)} 处 / 漏排 {len(no_duty)} / 脏行 {len(dirty)}")
for c in conflicts:
    print(f"冲突: {c[0]} 请假{c[1]}~{c[2]}({c[3]}) 撞值班日 {c[4]} 班次[{c[5]}] → 需代班")
for p in pending:
    print(f"未参与: {p[0]} 请假{p[1]}~{p[2]} 状态[{p[4]}] → 不参与冲突判定")

三个真坑(都是这轮实跑踩出来的)

状态过滤是「减少误报」,不是「删数据」:没参与的请假全部留在「未审批或被驳回」表里,审批一通过重跑一遍脚本,它就会进冲突判定。脚本每跑一次都是全量重算,不依赖上次结果。

懒得自己跑?把值班表和请假单发我(后台留言),AI 跑完人工核对后把冲突清单发回给你,9.9 元/次,虚拟物品交付后不退款、结果不符免费重做。更多现成脚本与代做服务 →

相关阅读:跨月值班表核对 · 值班表×请假单跨表核对(基础版) · 值班表排班冲突检测 · 考勤月度汇总表一键生成 · 打卡记录揪考勤异常

← 喵窝资源铺首页 · 全部文章