简介这份公司离职证明模板面向需要规范出具或获取劳动关系终止证明的职场人与企业人事围绕离职事实确认、日期与岗位信息填写、双方劳动关系解除声明等核心内容提供可直接套用的 docx 文本。模板预留姓名、入职及离职时间、职务、离职原因、工作表现与公司意见等字段并包含公司盖章与落款日期位置便于根据实际情况替换后打印使用适用于员工求职、社保及公积金业务、失业保险申领等场景。资源包共 1 个 docx 文件大小约 12KB轻量易编辑用 Word 或 WPS 即可打开修改。已有 1986 人学习下载说明其在同类文书需求中具有一定参考度。读者可获得一份结构完整、表述严谨的离职证明范本既能减少临时起草时的格式遗漏也便于核对关键条款与盖章信息提升文书办理效率。1. 一份「公司离职证明模板.docx」踩过三次坑之后它其实是一套待工程化的数据管道某公司的 HR 每个月要开二三十份离职证明做法是打开「公司离职证明模板.docx」把姓名、身份证号、入职日期、离职日期、岗位、部门逐个手改改完打印、盖章、扫描归档。前两个月没出事第三个月开始出问题有人的岗位写成了上一位离职同事的有人的离职日期跨月导致工龄多算了一个月还有一份证明上的身份证号少了一位。这些都不是态度问题而是因为那份 docx 从头到尾只是一个「长得像模板」的普通文档它没有字段、没有校验、没有留档全靠人眼当编译器。这篇讲的就是怎么把一个公司离职证明模板.docx 从「手改文档」变成「字段驱动 批量套打 可反查」的小型流水线用什么库生成骨架、占位符怎么命名、花名册怎么读、编号和哈希怎么留、以及哪几个坑会让你在盖章前一秒发现整批作废。适合手里管着几十到几百人花名册、又不想上重型 HR 系统的从业者。2. 拆字段把离职证明的抬头、正文、落款切成 12 个可替换变量2.1 离职证明的要素清单与占位符命名表动手写代码之前先把「哪些字是固定的、哪些字是每份都不一样的」分清楚。这一步做错后面所有脚本都白写。我一般会把一份离职证明切成三块抬头证明编号、开具日期、正文姓名、证件号、入职日期、离职日期、岗位、部门、工作年限、落款公司名、盖章位、经办人、日期。每一块里凡是会变的都必须有唯一占位符名命名规则统一用「小写下划线 语义前缀」不要用 name1、name2 这种。占位符含义类型格式约束数据来源{{ cert_no }}证明编号字符串前缀年月5位序号2位校验脚本生成{{ issue_date }}开具日期字符串YYYY年MM月DD日运行当天{{ name }}员工姓名字符串24 个汉字花名册{{ id_mask }}证件号脱敏字符串前 6 后 4中间 4 个星号花名册脱敏{{ join_date }}入职日期字符串YYYY年MM月DD日花名册{{ leave_date }}离职日期字符串YYYY年MM月DD日花名册{{ dept }}部门字符串与组织架构表一致花名册{{ title }}岗位字符串与劳动合同一致花名册{{ tenure }}工作年限字符串「X年X个月」脚本计算{{ company }}公司全称字符串与营业执照一致配置{{ operator }}经办人字符串24 个汉字配置{{ template_ver }}模板版本字符串v数字配置两个设计取舍值得说清楚。第一证件号在证明正文里默认脱敏只留前 6 后 4这是常见做法完整证件号如果需要放在单独的归档件里不放进发给员工的 PDF。第二工作年限不要让人手算tenure必须由脚本根据入职、离职两个日期算出来因为手算跨月的时候几乎必错尤其是 2 月 29 日和月末最后一天。2.2 用 python-docx 生成可复用的模板骨架模板不要靠手工在 Word 里敲手工敲出来的模板有三个问题字体不统一、占位符容易被输入法拆成两半、改一次版就要重新核对一遍。我一般用 python-docx 直接生成骨架好处是所有样式都写在代码里谁改了什么一目了然。# build_template.py from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn def set_font(run, name宋体, size12, boldFalse): 统一设置中西文字体避免 PDF 转换时回退到别的字体 run.font.name name # 西文字体 run.font.size Pt(size) run.bold bold run._element.rPr.rFonts.set(qn(w:eastAsia), name) # 中文字体必须单独设 def build(out_path: str): doc Document() sec doc.sections[0] sec.page_width, sec.page_height Cm(21.0), Cm(29.7) sec.left_margin sec.right_margin Cm(2.6) sec.top_margin sec.bottom_margin Cm(2.5) # 抬头编号 标题 p doc.add_paragraph() p.alignment WD_ALIGN_PARAGRAPH.RIGHT set_font(p.add_run(编号{{ cert_no }}), size10) p doc.add_paragraph() p.alignment WD_ALIGN_PARAGRAPH.CENTER set_font(p.add_run(离职证明), size22, boldTrue) # 正文整段一次性写入保证每个占位符落在单个 run 里 body ( 兹证明 {{ name }}证件号{{ id_mask }}于 {{ join_date }} 至 {{ leave_date }} 期间在我司 {{ dept }} 部门担任 {{ title }} 岗位 累计工作 {{ tenure }}。 ) p doc.add_paragraph() p.paragraph_format.first_line_indent Pt(24) p.paragraph_format.line_spacing 1.5 set_font(p.add_run(body), size12) # 落款右对齐的固定文字块 盖章位留白 for line in [{{ company }}, 盖章, {{ issue_date }}]: p doc.add_paragraph() p.alignment WD_ALIGN_PARAGRAPH.RIGHT set_font(p.add_run(line), size12) # 末尾版本号用于事后反查是哪一版模板出的证 p doc.add_paragraph() p.alignment WD_ALIGN_PARAGRAPH.RIGHT set_font(p.add_run(模板版本{{ template_ver }}), size9) doc.core_properties.title 离职证明模板 doc.core_properties.comments {{ template_ver }} doc.save(out_path) if __name__ __main__: build(公司离职证明模板.docx)这段代码有三个关键点。第一set_font里必须同时设run.font.name和w:eastAsia只设前者的话Word 里看着对转成 PDF 时中文会掉到默认字体上行宽一变就换行错位。第二正文那一大段用一次add_run写完这是刻意的占位符被拆到多个 run 里docxtpl 渲染时就有概率匹配不上。第三落款处的「盖章」不要用空格凑位置用右对齐段落盖章位的垂直空间靠段落间距撑开这样换字体也不会跑。标题里那个.docx后缀其实是个提醒真正要交付给员工的往往不是 docx而是 PDF。docx 只是中间产物任何人拿到 docx 都能改改完再打印你根本不知道经手的是哪一版。所以整条链路的终点必须是 PDFdocx 只在内部流转。2.3 版式约定要写进代码不要写在别人的记忆里模板类项目最常见的问题不是技术难而是「只有一个人知道该怎么改」。我一般会在模板脚本顶部固定几个常量正文字号 12 磅、行距 1.5、页边距 2.6 厘米、落款右对齐、编号字号 10 磅。这些数字不是随便定的是为了让一页 A4 刚好放下一段正文加落款不会因为多一个部门名就挤到第二页。部门名长度是变量最长可能到十几个字正文里要在dept后面留出余量宁可让正文折成两行也不要让落款被顶到下一页——因为落款和盖章位分页盖章的人第一眼就会觉得这份证明不对劲。这里还有个细节模板里的占位符周围不要加多余空格比如{{ name }}不要写成{{ name }}也不要写成{{name}}和{{ name}}两种混用。docxtpl 基于 Jinja2空格不影响渲染但人眼审核时容易漏掉尤其是在几十个占位符里找一个拼错的。3. 批量套打用 python-docx 与 docxtpl 从花名册一次生成 200 份证明3.1 花名册读入为什么一律按字符串读花名册一般是 Excel里面最容易出问题的三列是日期、证件号、以 0 开头的工号。如果直接用默认参数读pandas 会把日期推断成Timestamp渲染出来变成2024-03-15 00:00:00会把 18 位证件号推断成浮点数末几位变成 0会把007变成7。所以读入时一律按字符串读完之后自己校验格式。# load_roster.py import re import pandas as pd DATE_RE re.compile(r^\d{4}-\d{2}-\d{2}$) REQUIRED [name, id_no, join_date, leave_date, dept, title] def load_roster(xlsx_path: str) - pd.DataFrame: # dtypestr 是全流程最关键的一个参数缺了它后面全是玄学 df pd.read_excel(xlsx_path, dtypestr, keep_default_naFalse) df.columns [c.strip().lower() for c in df.columns] missing [c for c in REQUIRED if c not in df.columns] if missing: raise ValueError(f花名册缺列: {missing}) df df.apply(lambda s: s.str.strip()) df df[(df[name] ! ) (df[id_no] ! )] bad df[~df[join_date].str.match(DATE_RE) | ~df[leave_date].str.match(DATE_RE)] if not bad.empty: raise ValueError(f日期格式不合规行号: {list(bad.index 2)}) dup df[df.duplicated(id_no, keepFalse)] if not dup.empty: raise ValueError(f证件号重复: {sorted(set(dup[id_no]))}) return df.reset_index(dropTrue)dtypestr让所有列按原始文本读入keep_default_naFalse防止空单元格变成NaN字符串。后面三段校验分别挡住三类事故列名对不上改了表头没通知、日期写成2024/3/5Excel 常见的手输格式、同一个人出现两行复制粘贴留下的重复行。校验失败就直接抛异常不要「先跑完再人工挑」因为批量生成最怕的就是混进去一份错的盖章之后才发现。3.2 渲染主循环与 context 构造渲染用 docxtpl本质是把 docx 当 Jinja2 模板。有一个必须注意的点每一份都要重新DocxTemplate(...)实例化不能复用同一个对象连续 render否则上一份的 context 会渗到下一份里。# render_all.py import hashlib from datetime import date, datetime from pathlib import Path import pandas as pd from docxtpl import DocxTemplate from load_roster import load_roster COMPANY 某公司 OPERATOR 某经办人 TEMPLATE_VER v1 OUT_DIR Path(out_docx) def fmt_cn(d: str) - str: 2024-03-15 - 2024年03月15日 return datetime.strptime(d, %Y-%m-%d).strftime(%Y年%m月%d日) def tenure_cn(join: str, leave: str) - str: 按整月计算工作年限月末对齐避免跨月多算/少算 j datetime.strptime(join, %Y-%m-%d).date() l datetime.strptime(leave, %Y-%m-%d).date() months (l.year - j.year) * 12 (l.month - j.month) if l.day j.day: months - 1 months max(months, 0) return f{months // 12}年{months % 12}个月 def mask_id(id_no: str) - str: return id_no[:6] **** id_no[-4:] if len(id_no) 10 else id_no def make_cert_no(prefix: str, yyyymm: str, seq: int) - str: body f{prefix}{yyyymm}{seq:05d} chk sum(ord(c) * (i 1) for i, c in enumerate(body)) % 97 return f{body}-{chk:02d} def main(xlsx_path: str, tpl_path: str): df load_roster(xlsx_path) OUT_DIR.mkdir(parentsTrue, exist_okTrue) yyyymm date.today().strftime(%Y%m) ledger [] for i, row in df.iterrows(): cert_no make_cert_no(LZ, yyyymm, i 1) ctx { cert_no: cert_no, issue_date: date.today().strftime(%Y年%m月%d日), name: row[name], id_mask: mask_id(row[id_no]), join_date: fmt_cn(row[join_date]), leave_date: fmt_cn(row[leave_date]), dept: row[dept], title: row[title], tenure: tenure_cn(row[join_date], row[leave_date]), company: COMPANY, operator: OPERATOR, template_ver: TEMPLATE_VER, } tpl DocxTemplate(tpl_path) # 每份都重新加载杜绝状态残留 tpl.render(ctx, autoescapeTrue) out OUT_DIR / f{cert_no}_{row[name]}.docx tpl.save(out) ledger.append({cert_no: cert_no, name: row[name], file: out.name, template_ver: TEMPLATE_VER}) pd.DataFrame(ledger).to_csv(ledger.csv, indexFalse, encodingutf-8-sig) if __name__ __main__: main(roster.xlsx, 公司离职证明模板.docx)逐个参数看。tenure_cn里用「整月 日对齐」的算法l.day j.day时减一个月这样 3 月 31 日入职、4 月 30 日离职算 0 个月符合大多数公司的口径如果你们公司按自然月算把这个判断去掉即可但一定要在配置里写清楚不要让两个 HR 各算各的。mask_id保留了前 6 后 4长度不足时原样返回避免切片报错。autoescapeTrue是防止姓名或部门里出现、这类字符把 XML 弄坏虽然中文名基本不会但部门名里带括号和斜杠是很常见的。3.3 转 PDF 与盖章位预留docx 只是中间件最终的交付件是 PDF。命令行工具里最省事的是 LibreOffice 的无头模式一条命令批量转# 先确认字体已安装否则转出来一定跑版 fc-list :langzh | head -5 # 批量转 PDF输出到 out_pdf mkdir -p out_pdf libreoffice --headless --convert-to pdf --outdir out_pdf out_docx/*.docx第一条命令是排错用的如果fc-list里没有任何中文字体后面转出来的 PDF 里中文会变成方块或者被替换成别的字体行宽全变。容器环境里我一般直接装一套开源中文字体然后在模板里固定用「宋体」这个名字靠字体别名映射过去发行版不同映射方式不一样最稳的办法是模板里就用系统里真实存在的那个字体名。盖章位要在模板里留出足够的垂直空间一般落款上方留 3 到 4 行空白。如果贵司是电子章那就更要在 PDF 阶段做因为 docx 里的图片章很容易被后续编辑挪位置而 PDF 上加盖之后可以做哈希留档。提示批量转换完成后先随机抽 3 份打开看页数。任何一份超过 1 页说明正文或落款溢出了这时候不要去调字号先检查部门名和岗位名是不是超长。4. 编号与留档让每一份离职证明都能被反查到具体模板版本4.1 编号规则设计可读、可校验、不可猜编号这件事看着简单实际是整套流程里最容易留隐患的地方。我见过三种做法一是不编号靠文件名区分二是用 Excel 自动填充从 1 拉到 N三是按开具日期加随机数。前两种的问题是可反查性差——员工拿着证明来问「这份是不是你们开的」你没法快速定位第三种的问题是人工核对成本高。我一般用「业务前缀 年月 5 位序号 2 位校验位」。前缀区分文档类型离职证明用固定两位字母年月保证按批次可查序号在批次内连续校验位用加权和对 97 取模。校验位的作用不是防伪是防手抄错误员工报编号时抄错一位接收方一算校验位不对就能立刻发现。段位长度示例说明前缀2LZ固定区分文档类型年月6202403开具批次序号500017批次内连续不足补零校验241加权和对 97 取模编号的分配有个硬要求必须在渲染之前一次性分配完并且写进台账不能边渲染边生成。原因是渲染可能中途失败某一行数据有问题抛异常如果编号是边跑边生成的重跑一次就会跳号或者重号。正确顺序是先校验全表通过 → 分配编号写入台账 → 逐份渲染 → 渲染失败的单独重试编号不变。4.2 台账与哈希把每份 PDF 的指纹记下来台账不能只有文件名还要有内容指纹。PDF 生成之后算一次 SHA-256记进台账以后任何人拿一份 PDF 来重算哈希一比对就知道是不是原件、有没有被改过。这不是为了防谁是为了出事的时候能说清楚。# ledger_hash.py import hashlib from pathlib import Path import pandas as pd def sha256_file(p: Path) - str: h hashlib.sha256() with open(p, rb) as f: for chunk in iter(lambda: f.read(1 20), b): # 1MB 分块避免大文件吃内存 h.update(chunk) return h.hexdigest() def append_hash(ledger_csv: str, pdf_dir: str): df pd.read_csv(ledger_csv, dtypestr, keep_default_naFalse) pdf_dir Path(pdf_dir) hashes, sizes [], [] for name in df[file]: pdf pdf_dir / name.replace(.docx, .pdf) hashes.append(sha256_file(pdf) if pdf.exists() else ) sizes.append(pdf.stat().st_size if pdf.exists() else 0) df[sha256] hashes df[bytes] sizes df.to_csv(ledger_csv, indexFalse, encodingutf-8-sig) miss df[df[sha256] ] if not miss.empty: print(缺 PDF 的编号:, list(miss[cert_no]))hexdigest()出来是 64 位十六进制串直接存 CSV 没问题。分块读取是为了以后如果归档件从 PDF 换成带附件的压缩包函数不用改。bytes列存文件大小万一哈希列被误改大小对不上也是一个信号。台账本身要单独备份不要跟生成的 PDF 放在同一个目录里。原因很实际PDF 目录通常会给 HR 共享台账里有姓名和证件号脱敏后的信息权限应该收得更紧。4.3 模板版本号写进文档属性「这份证明是用哪一版模板出的」这个问题事后基本没人记得。解决办法是把版本号既写进正文末尾也写进 docx 的文档属性里doc.core_properties.title 离职证明模板 doc.core_properties.version TEMPLATE_VER doc.core_properties.comments 字段表-2024-03正文里的版本号是给人看的文档属性里的版本号是给脚本查的。转成 PDF 之后正文里的那一行会保留属性里的信息在大多数转换工具里会丢所以正文那一行不能省。版本号规则我一般用 v1、v2 递增每次改字段表或者改版式都必须递增并且台账里记录当时用的版本。这样一来如果某一版模板把「离职日期」的字段名写错了你能精确查出哪些证明受影响而不是全部重新开一遍。5. 排查清单离职证明模板套打里最容易出问题的 5 个现场5.1 占位符原样输出{{ name }}直接印在纸上现象批量生成 200 份其中十几份的姓名位置赫然印着{{ name }}其他位置都正常。原因模板在 Word 里被人手工编辑过。Word 会在你输入、拼写检查、切换输入法的过程中把一段文字拆成多个 runXML 片段占位符被拆到两个 run 里docxtpl 匹配时就漏掉了。docxtpl 会尽量合并同一段落内的 run但遇到w:proofErr、书签标记、修订标记时就会失手。解决三重防护。第一模板只用脚本生成禁止在 Word 里手工改占位符第二渲染后的每一份都做一次残留扫描第三扫描不通过直接让整批失败不要「生成完再人工挑」。import re, zipfile def assert_clean(docx_path: str): with zipfile.ZipFile(docx_path) as z: xml z.read(word/document.xml).decode(utf-8) left re.findall(r\{\{.*?\}\}, xml) if left: raise ValueError(f{docx_path} 残留占位符: {set(left)})5.2 日期变成2024-03-15 00:00:00证件号末几位变 0现象日期列渲染出来带时间18 位证件号最后三四位全变成 0工号007变成7。原因Excel 单元格本身带类型pandas 读入时按类型推断日期变Timestamp长数字串被转成float再转回来精度就丢了。解决read_excel加dtypestr, keep_default_naFalse读完之后再用正则校验格式不合格的当场报错。如果花名册是别人给的列类型不可控那就退一步读入后对日期列和证件号列各做一次astype(str)并剥离.0后缀但这只是补救治本还是从读入参数下手。5.3 本地看没问题转成 PDF 后落款跑到第二页现象docx 里一页放得下转 PDF 后落款和盖章位跑到第二页一半文件都这样。原因转换环境缺中文字体回退到了另一套字体字宽变宽正文多折了一行行距又没设死累积下来就把落款顶出去了。解决转换机器上装好中文字体模板里显式指定行距line_spacing 1.5而不是用默认单倍行距转换完成后统计每份 PDF 的页数超过 1 页的单独列出来。# 统计页数非 1 页的直接报出来 for f in out_pdf/*.pdf; do n$(pdfinfo $f | awk /^Pages/{print $2}) [ $n ! 1 ] echo 页数异常: $f - $n done5.4 编号重号或跳号现象两份证明编号相同或者序号从 00017 直接跳到 00019。原因一边渲染一边生成编号中间某一行抛异常中断重跑时序号从上次的进度继续或者从 1 重新开始。解决编号分配和渲染彻底分离。第一步全表校验第二步按行号一次性分配编号并写入台账第三步渲染失败的记下来单独重试、编号不变。台账里对编号列做唯一性断言重复就抛异常。5.5 模板里残留修订记录、批注和隐藏文字现象交付后发现 PDF 里带着一个不起眼的批注气泡或者 docx 属性里留着上一版作者的机器信息。原因模板被人用「修订」模式打开编辑过另存时批注、修订记录、rsid都还在或者模板是从别的文档另存来的文档属性沿用了旧值。解决模板发布前做一次清理用脚本检查word/comments.xml、word/people.xml是否存在存在就删core_properties.author、last_modified_by统一覆盖成固定值对外只发 PDFdocx 不出内网。注意清理元数据这一步不要写在渲染循环里放在模板构建阶段做一次就够了否则每份文件都要多解压一次 zip两百份就是两百次无谓 IO。6. 交付前自检三道校验把错证挡在盖章之前批量生成之后不要急着打印先跑三道校验把问题挡在盖章之前。第一道是结构校验每份 docx 渲染后扫描一遍残留占位符、检查页数、检查编号是否符合规则。第二道是数据校验拿台账跟花名册做一次左连接确认人数一致、编号唯一、入职日期早于离职日期。第三道是抽样校验随机抽 5%人眼比对姓名、日期、岗位三列。校验检查项失败处理结构残留占位符、页数1、编号格式整批中止修模板后重跑数据行数一致、编号唯一、日期先后定位到行号修花名册后重跑抽样姓名/日期/岗位三列人工比对记录到台账标注已复核第三道校验可以脚本化一半把抽样行的关键字段打印成一行文本跟 PDF 里的提取文本做包含判断。PDF 文本提取用pdfplumber之类都行判断逻辑很简单——抽样行的姓名和完整日期串必须能在对应 PDF 的文本里找到找不到就报出来。这一步能挡住大部分「字段错位」类的事故比如姓名写进了岗位的位置。import pdfplumber def check_sample(pdf_path: str, name: str, join_cn: str, leave_cn: str) - bool: with pdfplumber.open(pdf_path) as pdf: text .join(page.extract_text() or for page in pdf.pages) text text.replace( , ).replace(\n, ) return all(k in text for k in (name, join_cn, leave_cn))extract_text出来的文本会带换行和空格先做一次归一化再比对否则「2024年03月15日」被折成两行就永远匹配不上。说个我自己的教训。最早做这套东西的时候我把编号生成放进了渲染循环里觉得「反正一行代码」。结果第一次跑第 37 行因为部门名里有个全角空格导致校验失败抛了异常前面 36 份已经生成、编号已经用掉重跑时我图省事把起始序号改成了 37于是编号 37 到 72 的这批人台账里记的序号跟文件里的对不上后来有人来查一份三个月前的证明我对着两份表核了四十分钟。从那以后我的硬规矩是凡是「只能发生一次」的动作比如分配编号、生成哈希、写台账全部前置或后置到渲染循环之外渲染循环里只做纯函数式的拼装。这个习惯后来在别的模板类项目上也一直用值这个时间。如果你现在手里正拿着一份靠手改的公司离职证明模板.docx我的建议是先别急着上脚本花半小时把字段表列出来把所有会变的字圈出来给每个字段定一个不会重名的占位符。字段表定下来之后剩下的代码量其实不大一天能跑通。真正花时间的是那些边界日期口径、编号规则、留档方式这些定错了代码写得再漂亮也要返工。希望帮到你。本文还有配套的精品资源点击获取