夜雨聆风学习资料网

ARTICLE · 1098097

自研记账凭证PDF打印完整实操手册(含底层原理与成长感悟)

自研记账凭证PDF打印完整实操手册(含底层原理与成长感悟)

用财务软件打印凭证,最让人抓狂的不是打印本身,而是——

错位了只能反复试,分页错了只能碰运气,套打偏了只能找 IT。

后来我决定自己写一套。

兴奋,是因为可以自由调整;通透,是因为一切皆在掌控。

它不只是会用,更是会造。

本文适合三类读者:
  • 财务人员:想自己动手做凭证打印,不再受商用软件束缚;
  • 技术爱好者:想学 Python + ReportLab 生成 PDF,掌握精确到毫米的打印控制;
  • 终身学习者:喜欢“拆黑盒”,享受从现象到本质、从碎片到体系的认知快感。
你可以只读自己关心的部分。技术操作在第一至九节,认知复盘在第十节。
本文较长,建议先收藏,再按需阅读。

一、项目整体目标

本项目基于 Python、ReportLab 库与 SQLite 数据库,从零自研搭建一套记账凭证 PDF 自动打印系统。全程手动完成测量、建模、入库、渲染、调试全流程,不依赖商用软件,最终实现两大核心打印模式:空白 A4 白纸打印、财务专用凭证纸套打。
整套系统落地可实现三大核心能力:
手动测量空白纸质凭证,采集坐标数据生成专属打印模板,一次建模、永久复用;
手工录入纸质凭证的真实业务分录,实现每一张凭证业务数据独立存储;
程序自动匹配模板与业务数据,智能渲染分页、绘制凭证格式,输出标准可打印 PDF 文件。
名词注解:
  • Python:一种编程语言,语法接近英语,适合初学者,广泛用于数据处理、自动化脚本。
  • ReportLab:Python 的一个第三方库,专门用来生成 PDF 文件,可以精确控制文字、线条、表格的位置。
  • SQLite:一种轻量级数据库,不需要安装服务器,一个文件就是一个数据库,适合个人项目。
  • PDF:便携文档格式,打印时格式不会乱。
  • 模板:预先测量好的“格式框架”,规定格子、线条、文字的位置。
  • 渲染:把数据“画”到 PDF 上的过程,用代码代替手写。
  • 套打:在预制格式的凭证纸上,仅打印文字数字,不打印框线。

二、核心概念(整套系统的灵魂)

使用财务打印软件时,我们通常只接触打印按钮,却看不到背后的分层逻辑。
本项目最关键、最核心的设计,就是模板模具与业务数据彻底分离,这也是所有商用票据打印系统的通用底层架构。

1. 打印模板数据(固定模具)

对应四张模板数据表:template_main、template_entry_grid、template_text、template_line。
  • 数据来源:毫米直尺实测空白纸质凭证的线条、文字、表格坐标。
  • 存储内容:纸张尺寸、框线坐标、静态文字位置、字体字号、全局打印偏移量。
  • 核心特点:仅需采集一次,适配成千上万张凭证打印;不存储任何动态业务内容。

2. 凭证业务数据(动态内容)

对应两张业务数据表:voucher_head(凭证表头)、voucher_item(凭证明细)。
  • 数据来源:手工抄录真实发生的纸质记账凭证。
  • 存储内容:凭证字、凭证号、业务日期、附单据张数、摘要、科目、借贷金额。
  • 核心特点:每张凭证数据独立,通过 template_id 绑定模板,实现一套模具,万千内容。

3. 通俗类比理解

  • 模板= 印刷厂空白凭证底版,只定格式、固定不变;
  • 业务数据= 会计手写的每一笔分录,每张凭证各不相同。
PDF 渲染的本质:固定模具承载动态业务数据,分层叠加、精准合成。

4. 六表关系图

template_main(模板主表)  ├── template_entry_grid(分录网格)  ├── template_text(静态文字)  └── template_line(固定线条)voucher_head(凭证表头)  └── voucher_item(凭证明细)voucher_head.template_id → template_main.template_id
建表之前脑子里先有这张图,全程不会懵。

三、前期环境与规范准备

1. 所需软件与工具

运行环境:Anaconda
核心依赖:ReportLab
pip install reportlab
数据库工具:DB-Browser-for-SQLite(可视化、零复杂 SQL)

2. 所需硬件物料

空白记账凭证、已填写分录凭证、A4 白纸、毫米直尺。
统一项目目录:D:\voucher_project,所有文件统一存放。

3. 坐标系硬性标准(精准打印核心)

A4 左下角为坐标原点:X=0,Y=0;
X 向右递增、Y 向上递增,单位统一 mm;
3.文字 Y 坐标严格以基线为准:以汉字“一”横画下边缘对齐,杜绝文字裁切、错位;
分录横线由代码动态生成,无需手动测量。

四、数据库建表实操

打开 DB-Browser-for-SQLite,新建 voucher_print.db,执行以下 SQL,生成4 张模板表+2 张业务表。
-- ========= 4张打印模板表(模具,空白凭证测量得到) =========CREATE TABLE IF NOT EXISTS template_main (    template_id TEXT PRIMARY KEY,    template_name TEXT NOT NULL,    paper_width_mm REAL,    paper_height_mm REAL,    max_entry_line_per_page INTEGER,    line_height_mm REAL,    offset_x REAL DEFAULT 0,    offset_y REAL DEFAULT 0,    print_frame_line INTEGER DEFAULT 1 --1白纸打印画框;0套打不画框);CREATE TABLE IF NOT EXISTS template_entry_grid (    template_id TEXT PRIMARY KEY REFERENCES template_main(template_id),    left_x_mm REAL,    right_x_mm REAL,    first_line_top_y_mm REAL,    col1_x_mm REAL,    col2_x_mm REAL,    col3_x_mm REAL);CREATE TABLE IF NOT EXISTS template_text (    id INTEGER PRIMARY KEY AUTOINCREMENT,    template_id TEXT REFERENCES template_main(template_id),    text_code TEXT NOT NULL,    desc TEXT,    x_mm REAL,    y_mm REAL,    font_name TEXT,    font_size_pt INTEGER,    align TEXT CHECK(align IN ('left','right')));CREATE TABLE IF NOT EXISTS template_line (    id INTEGER PRIMARY KEY AUTOINCREMENT,    template_id TEXT REFERENCES template_main(template_id),    line_name TEXT,    x1_mm REAL,    y1_mm REAL,    x2_mm REAL,    y2_mm REAL,    line_width REAL);-- ========= 2张业务数据表(存储一张张填好的纸质凭证内容) =========CREATE TABLE IF NOT EXISTS voucher_head (    vid INTEGER PRIMARY KEY AUTOINCREMENT,    template_id TEXT, --关联使用哪一套打印模板    voucher_word TEXT,    voucher_no TEXT,    voucher_date TEXT,    attach_count INTEGER,    create_time TEXT DEFAULT CURRENT_TIMESTAMP);CREATE TABLE IF NOT EXISTS voucher_item (    iid INTEGER PRIMARY KEY AUTOINCREMENT,    vid INTEGER, --关联voucher_head的vid,归属某一张凭证    summary TEXT,    subject TEXT,    debit REAL DEFAULT 0,    credit REAL DEFAULT 0,    FOREIGN KEY (vid) REFERENCES voucher_head(vid));

五、打印模板采集录入(一次采集、永久复用)

1. 纸张摆放规范(重中之重)

A4 白纸与空白凭证纸左下角、底边、左边完全对齐、固定不动。
基准错一寸,全局错一尺。

2-5. 模板数据录入

依次录入:模板主表、分录网格、静态文字、固定线条。
核心原则:模板只存位置,不存内容;业务只存内容,不存位置。

6. 模板自检清单

基准对齐、单位统一毫米、文字基线测量、无多余分录横线、偏移归零、坐标方向无误。

六、凭证业务数据录入

针对每一张纸质凭证,录入表头信息与凭证明细,绑定对应模板 ID。
模板定型不动,业务数据灵活可变。

七、Python 渲染核心代码

新建voucher_all.py,完整代码如下:
import sqlite3from reportlab.lib.pagesizes import A4from reportlab.pdfgen import canvasfrom reportlab.lib.units import mmfrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.cidfonts import UnicodeCIDFont注册中文字体      pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light'))             def safe_text(text, max_len=24):          ”””文本超长简单截断”””          s = str(text)          if len(s) > max_len:              return s[:max_len-3] + ”…”          return s             def load_template_from_db(db_path, template_id):          ”””从数据库读取打印模板(模具)”””          conn = sqlite3.connect(db_path)          conn.row_factory = sqlite3.Row          cur = conn.cursor()          cur.execute(”SELECT * FROM template_main WHERE template_id=?”, (template_id,))          main_row = cur.fetchone()          if not main_row:              raise Exception(f”数据库找不到模板 template_id={template_id}”)          cur.execute(”SELECT * FROM template_entry_grid WHERE template_id=?”, (template_id,))          grid_row = cur.fetchone()          cur.execute(”SELECT * FROM template_text WHERE template_id=?”, (template_id,))          text_rows = cur.fetchall()          cur.execute(”SELECT * FROM template_line WHERE template_id=?”, (template_id,))          line_rows = cur.fetchall()          conn.close()                 template = {              ”offset_x”: main_row[”offset_x”],              ”offset_y”: main_row[”offset_y”],              ”max_entry_line_per_page”: main_row[”max_entry_line_per_page”],              ”line_height_mm”: main_row[”line_height_mm”],              ”print_frame_line”: main_row[”print_frame_line”],              ”entry_grid”: {                  ”left_x”: grid_row[”left_x_mm”],                  ”right_x”: grid_row[”right_x_mm”],                  ”first_line_top_y”: grid_row[”first_line_top_y_mm”],                  ”col_x”: [                      grid_row[”left_x_mm”],                      grid_row[”col1_x_mm”],                      grid_row[”col2_x_mm”],                      grid_row[”col3_x_mm”]                  ]              },              ”header_text”: [],              ”footer_text”: [],              ”all_fixed_line”: []          }                 for r in text_rows:              item = {                  ”code”: r[”text_code”],                  ”x”: r[”x_mm”],                  ”y”: r[”y_mm”],                  ”font_name”: r[”font_name”],                  ”fontSize”: r[”font_size_pt”],                  ”align”: r[”align”],                  ”text”: r[”desc”]              }              if r[”text_code”] in (”voucher_word”,”voucher_no”,”voucher_date”,”attach_count”):                  template[”header_text”].append(item)              else:                  template[”footer_text”].append(item)                 for r in line_rows:              line_item = {                  ”x1”: r[”x1_mm”],                  ”y1”: r[”y1_mm”],                  ”x2”: r[”x2_mm”],                  ”y2”: r[”y2_mm”],                  ”line_width”: r[”line_width”]              }              template[”all_fixed_line”].append(line_item)          return template             def load_voucher_from_db(db_path, vid):          ”””读取凭证业务数据”””          conn = sqlite3.connect(db_path)          conn.row_factory = sqlite3.Row          cur = conn.cursor()          cur.execute(”SELECT * FROM voucher_head WHERE vid=?”, (vid,))          head_row = cur.fetchone()          if not head_row:              raise Exception(f”找不到凭证 vid={vid}”)          cur.execute(”SELECT * FROM voucher_item WHERE vid=?”, (vid,))          item_rows = cur.fetchall()          conn.close()                 voucher_info = {              ”voucher_word”: head_row[”voucher_word”],              ”voucher_no”: head_row[”voucher_no”],              ”voucher_date”: head_row[”voucher_date”],              ”attach_count”: str(head_row[”attach_count”])          }          entry_data = []          for r in item_rows:              entry_data.append({                  ”summary”: r[”summary”],                  ”subject”: r[”subject”],                  ”debit”: r[”debit”],                  ”credit”: r[”credit”]              })          return head_row[”template_id”], voucher_info, entry_data             def draw_voucher(c, template, voucher_data, entry_list):          ”””核心渲染函数”””          ox = template[”offset_x”] * mm          oy = template[”offset_y”] * mm          max_entry = template[”max_entry_line_per_page”]          line_h = template[”line_height_mm”] * mm          print_frame = template[”print_frame_line”]                 total_debit_all = sum(item[”debit”] for item in entry_list)          total_credit_all = sum(item[”credit”] for item in entry_list)          if abs(total_debit_all - total_credit_all) > 0.001:              raise ValueError(f”借贷不平衡!借方:{total_debit_all:.2f} 贷方:{total_credit_all:.2f}”)                 page_groups = [entry_list[i:i+max_entry] for i in range(0, len(entry_list), max_entry)]          total_page = len(page_groups)          grid = template[”entry_grid”]                 entry_left_x = grid[”left_x”] * mm + ox          entry_right_x = grid[”right_x”] * mm + ox          table_top_y = grid[”first_line_top_y”] * mm + oy          col_x_list = [x * mm + ox for x in grid[”col_x”]]                 for page_idx, page_entries in enumerate(page_groups):              if page_idx > 0:                  c.showPage()                     if print_frame == 1:                  for line in template[”all_fixed_line”]:                      x1 = line[”x1”] * mm + ox                      y1 = line[”y1”] * mm + oy                      x2 = line[”x2”] * mm + ox                      y2 = line[”y2”] * mm + oy                      c.setLineWidth(line[”line_width”])                      c.line(x1, y1, x2, y2)                     for t in template[”header_text”]:                  x = t[”x”] * mm + ox                  y = t[”y”] * mm + oy                  c.setFont(t[”font_name”], t[”fontSize”])                  val = safe_text(voucher_data[t[”code”]])                  if t[”align”] == ”left”:                      c.drawString(x, y, val)                  else:                      c.drawRightString(x, y, val)                     header_y = table_top_y              c.setFont(”STSong-Light”, 10)              c.drawString(col_x_list[0]+1mm, header_y+1mm, ”摘要”)              c.drawString(col_x_list[1]+1mm, header_y+1mm, ”会计科目”)              c.drawRightString(col_x_list[2]-1mm, header_y+1mm, ”借方金额”)              c.drawRightString(col_x_list[3]-1mm, header_y+1mm, ”贷方金额”)                     if print_frame == 1:                  c.line(entry_left_x, header_y, entry_right_x, header_y)                     current_y = table_top_y - line_h              page_bottom_y = current_y - len(page_entries)line_h                     if print_frame == 1:                  for xv in col_x_list:                      c.line(xv, table_top_y, xv, page_bottom_y)                     for entry in page_entries:                  c.setFont(”STSong-Light”, 10)                  c.drawString(col_x_list[0]+1mm, current_y+1mm, safe_text(entry[”summary”]))                  c.drawString(col_x_list[1]+1mm, current_y+1mm, safe_text(entry[”subject”]))                  c.drawRightString(col_x_list[2]-1mm, current_y+1mm, f”{entry['debit']:.2f}”)                  c.drawRightString(col_x_list[3]-1mm, current_y+1*mm, f”{entry['credit']:.2f}”)                  if print_frame == 1:                      c.line(entry_left_x, current_y, entry_right_x, current_y)                  current_y -= line_h                     for t in template[”footer_text”]:                  x = t[”x”] * mm + ox                  y = t[”y”] * mm + oy                  c.setFont(t[”font_name”], t[”fontSize”])                  if t[”code”] == ”debit_total”:                      txt = f”借方合计:{total_debit_all:.2f}” if (page_idx == total_page-1) else ”借方合计:”                  elif t[”code”] == ”credit_total”:                      txt = f”贷方合计:{total_credit_all:.2f}” if (page_idx == total_page-1) else ”贷方合计:”                  else:                      txt = t[”text”]                  if t[”align”] == ”left”:                      c.drawString(x, y, txt)                  else:                      c.drawRightString(x, y, txt)             if name == ”main”:          DB_FILE = r”voucher_print.db”          VOUCHER_VID = 1          used_template_id, voucher_info, entry_data = load_voucher_from_db(DB_FILE, VOUCHER_VID)          template = load_template_from_db(DB_FILE, used_template_id)          pdf_name = ”输出记账凭证.pdf”          c = canvas.Canvas(pdf_name, pagesize=A4)          try:              draw_voucher(c, template, voucher_info, entry_data)              c.save()              print(f”✅成功生成 {pdf_name}”)          except Exception as e:              c.save()              print(f”❌渲染出错:{e}”)    
运行命令:
cd D:\voucher_projectpython voucher_all.py

八、打印调试与模式切换

整体错位调偏移,局部错位调坐标。
  • 白纸打印:print_frame_line=1,自动绘制全套框线
  • 凭证套打:print_frame_line=0,仅输出文字数字

九、故障排查方案

中文乱码、文字裁切、整体偏移、多余线条、模板找不到等问题,均可通过对应坐标、参数、基准快速修复。

十、项目核心原理与个人成长感悟

1. 项目核心原理

从前使用商用财务软件,打印是封闭黑盒:出错只能试错、靠运气、求人解决。
自研这套系统后彻底通透:
所有票据、凭证、报表打印的底层逻辑,全都是:模板定样式、数据定内容、代码定规则。
模板与数据彻底解耦,是所有成熟打印系统的工业级设计。

2. 认知复盘

逐段弄懂坐标系基线规则、模板业务分离、全局偏移微调、自动分页、套打开关、借贷校验的底层原理时,内心满是通透与兴奋。后来,我慢慢读懂了这份感受。
这六个技术点,串联起整套打印系统的完整链路:
定位 → 架构 → 校正 → 分页 → 输出模式 → 数据校验
打通整条链路的那一刻,不是学会了功能,是拆解了系统。

兴奋,是因为可以自由调整;通透,是因为一切皆在掌控。

兴奋,是手里有了修改的权利,不再被动接受软件的固定格式;通透,是心里有了预判的逻辑,任何异常都不再是玄学。

懂得了原理,就像提前知道了答案,带来一种确定感与十拿九稳。并非拿到现成结果,而是掌握了推演的逻辑;现象不再是随机的意外,一切变化都在掌控之内。

普通人靠经验试错,懂原理的人靠逻辑推演。
从前打印错位是随机故障,现在是可溯源、可解释、可修复的链路问题。确定感,永远来自底层逻辑,不来自运气。
这份兴奋,是认知破局带来的多巴胺。
原本神秘的黑盒被亲手拆解,杂乱的现象收敛成自洽的闭环,不确定性被彻底消解。从“被动使用工具”变成“主动设计规则”,这是使用者到创造者的蜕变。
这份通透,是掌控感回归的踏实。
从前是工具束缚人,报错、错位、分页异常只能反复试错;如今是人驾驭工具,清楚每一处参数的边界、每一次改动的影响。
这份愉悦,来自完整体系的逻辑闭环。
从物理纸张测量、数据库建模、代码渲染,到调试定型,全链路无断点、无盲区。这种通透的清爽感,是碎片化学习永远给不了的。
很多人学习只满足于“能用、会用”,而真正的成长,在于“弄懂、看透、掌控”。
它不只是会用,更是会造。
这套项目留给我的,不只是一套打印代码,更是深挖底层、拆解黑盒、闭环落地的思维习惯。
技术成长从来不是堆砌代码,而是读懂底层原理、搭建心智模型、沉淀可迁移复用的思维体系。
每打通一个原理、完成一次闭环,就是一次扎实的成长。深耕底层、循序渐进,就是最稳的技术之路。

3. 总评

这次自研最大的收获,不是实现了凭证打印,而是完成了一次完整的认知跃迁:
从现象到本质,从碎片到体系,从被动到主动,从使用到创造。
能拆、能懂、能改、能造,才是真正掌握一门技术。

如果这篇实操手册对你有帮助,欢迎收藏、转发。
评论区聊聊:你遇到过最折磨人的软件打印问题是什么?
关注我,持续更新更多「从零自研、拆解底层」的完整项目复盘。

相关学习资料