ARTICLE · 1100182
第381讲:VBA(Office 原生)和 Python(跨平台批量)实现报销单/发票批量生成 Word/PDF
每到月底,财务同事的桌面大概是这样的:一个塞满明细的 Excel、十几个手动复制出来的 Word 报销单、命名从“报销单1”到“报销单final真最终版”……复制、粘贴、调格式、另存为 PDF,一套流程下来,两三个小时就没了,还容易串行。

这一讲,我们不讲“自动化神话”,只解决一个非常具体的活:把 Excel 报销明细,一键灌进 Word 模板,批量导出带编号的 PDF,并规规矩矩地归档。
我会同时给两套实现:VBA(Office 原生)和 Python(跨平台批量),你可以按团队环境选。
一、先理清业务骨架
假设 Excel 结构如下:
报销编号 | 姓名 | 部门 | 事由 | 金额 | 日期 |
|---|---|---|---|---|---|
BX20240901 | 张三 | 市场部 | 差旅 | 1860.50 | 2026-09-01 |
目标产物:
每个报销单一个 PDF
文件名规则:
BX20240901_张三_报销单.pdf归档目录:
归档/2024/09/
这个“数据→模板→PDF→归档”的链路,核心只有两步:填充模板 + 导出固定格式。
二、VBA 方案:Office 内邮件合并的“老派稳”
VBA 的优势是不用装环境,财务电脑自带。思路用 Word 的邮件合并(MailMerge),再用 ExportAsFixedFormat落 PDF。

关键步骤
在 Word 里做好模板,占位符用
«姓名» «报销编号»这类合并域。Excel 作为数据源连接。
逐条执行合并 → 另存 PDF。
核心 VBA 片段(Word端)
Sub BatchExportPDF()Dim wd As Document, rec As LongSet wd = ActiveDocumentwd.MailMerge.DataSource.ActiveRecord = 1For rec = 1 To wd.MailMerge.DataSource.RecordCountwd.MailMerge.Destination = wdSendToNewDocumentwd.MailMerge.Execute FalseWith ActiveDocument.ExportAsFixedFormat _OutputFileName:="D:\归档\2024\09\" & _.MailMerge.DataFields("报销编号").Value & "_报销单.pdf", _ExportFormat:=wdExportFormatPDF.Close FalseEnd Withwd.MailMerge.DataSource.ActiveRecord = rec + 1NextEnd Sub
💡 踩坑提示:邮件合并对“金额”默认会丢小数位,需在 Excel 里先把金额格式成文本
"1860.50",或在域里用\# "0.00"开关。
适用场景:财务只跑 Windows + Office,模板样式复杂(页眉页脚、骑缝章位置),VBA 最省事。
三、Python 方案:批量、可复现、跨平台
当一次要跑几百条、或者要在 Linux 服务器/CI 里定时生成,VBA 就力不从心了。Python 用 python-docx填模板,Jinja2做文本渲染,docx2pdf(Win)或 LibreOffice headless(跨平台)转 PDF。

方案选型对照
环节 | 库 | 说明 |
|---|---|---|
读Excel |
| 稳定读 xlsx |
填Word |
| 替换占位符 |
复杂排版 |
| 适合循环明细行 |
转PDF |
| 跨平台首选后者 |
Python 完整示例
import pandas as pdfrom docx import Documentimport osfrom docx2pdf import convert # Windows环境TEMPLATE = "报销单模板.docx"OUT_DIR = "归档/2024/09"os.makedirs(OUT_DIR, exist_ok=True)df = pd.read_excel("报销明细.xlsx")def fill_template(row):doc = Document(TEMPLATE)# 简单占位符替换(段落+表格单元格)mapping = {"{{报销编号}}": str(row["报销编号"]),"{{姓名}}": row["姓名"],"{{部门}}": row["部门"],"{{事由}}": row["事由"],"{{金额}}": f'{row["金额"]:.2f}',"{{日期}}": str(row["日期"])[:10],}for p in doc.paragraphs:for k, v in mapping.items():if k in p.text:p.text = p.text.replace(k, v)# 处理表格内占位符for tbl in doc.tables:for r in tbl.rows:for cell in r.cells:for k, v in mapping.items():if k in cell.text:cell.text = cell.text.replace(k, v)docx_path = f"{OUT_DIR}/{row['报销编号']}_tmp.docx"doc.save(docx_path)return docx_pathfor _, row in df.iterrows():tmp = fill_template(row)pdf_path = tmp.replace("_tmp.docx", f"_{row['姓名']}_报销单.pdf")convert(tmp, pdf_path)os.remove(tmp)print("✅ 批量生成完成")
📌 进阶干货:如果报销单要带“多行费用明细”(一条报销含机票/酒店/打车多行),纯占位符不够用。此时用 Jinja2 渲染 HTML → WeasyPrint 转 PDF,比硬改 docx 表格优雅得多:
from jinja2 import Template
html = Template(open("tpl.html").read()).render(rows=detail_rows)
# WeasyPrint(html, writer).write("out.pdf")
Python vs VBA 实战取舍
维度 | VBA | Python |
|---|---|---|
环境 | Office自带 | 需装Python |
样式还原 | 100%(原生Word) | docx还原好,HTML转PDF更灵活 |
批量几百条 | 慢、易卡 | 秒级 |
定时/服务端 | 不支持 | 配合cron/任务计划 |
学习成本 | 财务易上手 | 需一点开发基础 |
四、交付规范(别只给文件,给“规矩”)
自动化不是只出PDF,而是让归档可审计:

编号规则:
BX + 年份 + 月份 + 3位流水,如BX202409001文件名:
{编号}_{姓名}_报销单.pdf目录结构:
归档/└── 2024/└── 09/├── BX202409001_张三_报销单.pdf└── 生成日志_20240930.txt日志:记录每条生成状态(成功/跳过/金额异常),财务留痕用。
小经验:金额>5000的报销单,建议在文件名前加标记
[审],方便复核人一眼筛。
五、随堂小测(5道选择题)
1. 使用 VBA 批量导出 Word 为 PDF,核心方法是?
A. SaveAs2
B. ExportAsFixedFormat
C. PrintOut
D. MailMerge.Execute
2. Python 中用于读取 Excel 报销明细的常用库是?
A. python-docx
B. Jinja2
C. pandas / openpyxl
D. pdfkit
3. 关于邮件合并“金额丢小数”问题,下列最稳妥的处理是?
A. 忽略,四舍五入即可
B. Excel 预格式化为文本 "1860.50"或用 \# "0.00"域开关
C. 在Word里手改
D. 改用PPT做单
4. 需要“一条报销含多行费用明细”的复杂排版,推荐方案是?
A. VBA 邮件合并硬塞
B. Jinja2渲染HTML + WeasyPrint转PDF
C. 手动复制表格
D. 截图贴图
5. 下列文件名符合本讲编号规范的是?
A. 报销单_final.docx
B. 张三.pdf
C. BX202409001张三报销单.pdf
D. 9月1号.pdf
🔑 答案(拉到底才看哦)
B 2. C 3. B 4. B 5. C