ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

从数据模型到套打出票:房租水电费收据打印系统落地实践

从数据模型到套打出票:房租水电费收据打印系统落地实践 简介面向房东、物业及小微企业收款人员的房租水电费收据打印工具聚焦房租、押金、水费、电费、燃气费等日常收款场景意在替代手写收据和Excel对账的繁琐流程。软件将收据管理、出租房管理、客户资料库与数据分析整合在一起支持即输即打、单据编号自定义、金额自动计算与大小写转换、批量打印、按收据生成对账单并可将数据或单据导出为Excel、图片、PDF提供打印样式自定义及导入导出、数据备份恢复、U盘便携使用等功能。包体为zip压缩包约3.39MB上游未提供可核验的文件总数与主要文件类型明细具体以实际包内内容为准。资源页显示已有292人浏览学习适合需要规范开具房租水电收据、减少手工录入与对账差错的个人房东、公寓管理者和中小商户参考使用。1. 房租水电费收据打印把月底最脏的活压成一条命令月底最后一天二三十套房、四五十个租户的水电表数还散在两三个 Excel 和一堆聊天记录里手写收据写到手腕发酸写错一个字整张重来。房租水电费收据打印这件事真正麻烦的地方不在打印而在把抄表数、租金、水电单价、上期欠费合成一张能直接撕给租户的两联票据同时把编号、留底、跨月对账一起管住。它不适合大型物业的全套计费系统但对付几十套自持房源、每月集中开一次票的场景比通用财务软件顺手得多。这篇按我实际落地的顺序走先把数据模型立住再解决算钱再解决套打坐标最后讲那些只有真打过错位票据才懂的坑。2. 收据的数据底座房间、租户、抄表、账单四张表怎么拆2.1 房租水电费收据打印为什么不能只靠一张 Excel 模板用 Excel 做邮件合并出收据第一年确实够用。房源数量一过二十套问题就一茬接一茬地冒出来编号靠手填两个人同时开单必然撞号收据打废了重打一张旧号是作废还是留着表格里根本看不出来租户中途退租按天折算的租金在下个月对账时已经找不到计算依据更麻烦的是历史数据——你想查某个房间过去十二个月的水电用量曲线得手工把十二张表拼起来。把数据拆成四张表的理由很朴素房间和租户是相对稳定的事实抄表读数和账单是按月追加的事件。稳定的事实改动少、可以放心做外键按月追加的事件只允许插入、不允许覆盖否则一旦算错连回滚的基准都没有。账单和收据还要再拆一层——账单是可重算的业务结果收据是已经交给租户的凭证两者的生命周期完全不同。账单算错了可以作废重开收据一旦递出去只能标记作废后另开一张新号的绝不能改数字。这个拆分带来的直接好处是出票失败、打印机卡纸、租户要求补打都只是往收据表里插一条新记录账单本身不动月底对账时应收看账单表已开票看收据表两者的差额就是还没送到租户手上的那部分工作量。2.2 四张表的建表语句与字段取舍SQLite 最省事单文件、免安装、拷走就能换机器和这类轻量工具的气质匹配。下面这套结构在生产里跑了两年多改动很少。-- 金额统一用 INTEGER 存分任何情况下不落 REAL CREATE TABLE room ( id INTEGER PRIMARY KEY, code TEXT NOT NULL UNIQUE, -- 房号A-302导入前统一大写去空格 area REAL NOT NULL DEFAULT 0, -- 建筑面积只用于公摊分摊 rent_cent INTEGER NOT NULL, -- 月租金分 status TEXT NOT NULL DEFAULT vacant -- vacant / rented ); CREATE TABLE tenant ( id INTEGER PRIMARY KEY, room_id INTEGER NOT NULL REFERENCES room(id), name TEXT NOT NULL, phone TEXT, start_date TEXT NOT NULL, -- ISO 86012024-03-01 end_date TEXT, -- NULL 表示在租 deposit_cent INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE meter_reading ( id INTEGER PRIMARY KEY, room_id INTEGER NOT NULL REFERENCES room(id), kind TEXT NOT NULL, -- water / electric read_at TEXT NOT NULL, -- 抄表日 value REAL NOT NULL, -- 本期表读数 prev_value REAL, -- 上期读数冗余存一份用于校验与追溯 source TEXT DEFAULT manual -- manual / photo / import ); CREATE TABLE bill ( id INTEGER PRIMARY KEY, room_id INTEGER NOT NULL REFERENCES room(id), period TEXT NOT NULL, -- 2024-06 rent_cent INTEGER NOT NULL DEFAULT 0, water_cent INTEGER NOT NULL DEFAULT 0, elec_cent INTEGER NOT NULL DEFAULT 0, other_cent INTEGER NOT NULL DEFAULT 0, -- 卫生费、车位、滞纳金 total_cent INTEGER NOT NULL, status TEXT NOT NULL DEFAULT open, -- open / paid / void UNIQUE (room_id, period) -- 同一房间同一账期只能有一张有效账单 ); CREATE TABLE receipt ( id INTEGER PRIMARY KEY, bill_id INTEGER NOT NULL REFERENCES bill(id), serial TEXT NOT NULL UNIQUE, -- 收据号全局唯一 printed_at TEXT, status TEXT NOT NULL DEFAULT valid -- valid / reprint / void ); CREATE INDEX idx_bill_period ON bill(period); CREATE INDEX idx_receipt_bill ON receipt(bill_id);几个字段的取值不是随手定的字段类型为什么这么设rent_cent/total_centINTEGER 分浮点乘 100 会出现 12999.999999 这类值取整方向一错就差一分prev_valueREAL 冗余上期读数本该从上一行推导但跨年、换表、换租户时推导会断UNIQUE(room_id, period)约束防止重复算费时插出两张账单月底对账直接翻倍serialUNIQUE编号重复是收据类工具最致命的错靠约束兜底status文本枚举作废用状态位表达不 DELETE留痕比省空间重要注意不要用是否已打印来控制收据能否重打用status加一张receipt_log表记流水。状态位丢失可以重建流水丢了就真丢了。2.3 三个必须提前定死的约束编号唯一且不可复用。编号规则常见的是前缀 年月 四位流水的SD2024060001。生成时不要用SELECT MAX(serial)1那是并发下的经典翻车点。要么单独建一张counter(period, next_no)表在事务里自增要么直接依赖serial 的 UNIQUE 约束做插入失败就重试三次。后者实现最省事import sqlite3, time def next_serial(conn, period: str, prefix: str SD) - str: 在事务里自增计数器SQLite 的写锁是库级的天然串行 with conn: # with 块自动 commit / rollback conn.execute(INSERT OR IGNORE INTO counter(period, next_no) VALUES (?, 1), (period,)) row conn.execute(SELECT next_no FROM counter WHERE period?, (period,)).fetchone() no row[0] conn.execute(UPDATE counter SET next_no? WHERE period?, (no 1, period)) return f{prefix}{period.replace(-, )}{no:04d}这里的关键是把取号和占号放在同一个写事务里中途崩溃会整体回滚不会出现号被吃掉的情况。如果业务允许跳号作废的号保留空缺把next_no的自增和收据插入也放进同一事务失败就整笔重来。抄表记录只追加。抄错了怎么办插一条sourcecorrect的新记录用read_at的时间戳排序取最新而不是 UPDATE 旧行。原因是月底对账时经常有人问这户上个月到底抄的是多少改过的记录回答不了这个问题。金额只存整数分。这条约束要从数据入口一直贯穿到打印中间任何一次float * 100都是隐患。取整方向统一用四舍五入到分不要一会儿int()截断一会儿round()。2.4 从旧 Excel 导历史数据一次性的迁移脚本换工具最大的阻力从来不是功能是老数据搬不过去。房源表通常是最规整的一张先导它。import re, sqlite3 from decimal import Decimal from openpyxl import load_workbook def to_cent(v) - int: 任何输入先转字符串再进 Decimal绕开 float 的二进制误差 return int((Decimal(str(v)) * 100).quantize(Decimal(1))) def import_rooms(xlsx_path: str, db_path: str) - None: wb load_workbook(xlsx_path, data_onlyTrue) # data_only 取公式结果而不是公式文本 ws wb.active conn sqlite3.connect(db_path) ok skip 0 for row in ws.iter_rows(min_row2, values_onlyTrue): code, area, rent row[0], row[1], row[2] if not code or rent in (None, ): skip 1 continue # 去空格、统一大写避免 A-302 / a302 / A 302 在库里变成三个房间 code re.sub(r\s, , str(code)).upper() cur conn.execute( INSERT OR IGNORE INTO room(code, area, rent_cent) VALUES (?,?,?), (code, float(area or 0), to_cent(rent)), ) ok cur.rowcount conn.commit() conn.close() print(f导入 {ok} 条跳过 {skip} 条)data_onlyTrue这个参数容易被忽略不加的话单元格里如果是公式读出来是B2*C2这种字符串Decimal直接抛异常。INSERT OR IGNORE配合code的 UNIQUE 约束让脚本可以反复跑跑第二遍不会插重复行这对迁移期反复试错很友好。跑完一定要抽三条数据手动核对租金金额尤其是表格里带千分位逗号的写法——1,200进Decimal会直接报错宁可让脚本炸掉也不要静默跳过。3. 从抄表数字到应收金额租金折算、阶梯水电和金额大写的写法3.1 计费规则的三种常见形态与边界几十套房源的计费规则听上去简单真落到代码里边界情况比规则本身多计费项常见规则容易漏的边界月租金固定金额按月收中途入住/退租按天折算大小月天数不同水费本期读数减上期读数乘单价换表导致读数为 0抄表跨月漏抄电费阶梯计价或单一单价阶梯跨档时的分段计算公摊电量按户/按面积其他卫生费、车位、滞纳金滞纳金是否计入本金、是否参与下期滚动按天折算这块最容易扯皮。行业里两种算法按当月实际天数折算或者统一按 30 天折算。前者更公平但每个月的日租金都不一样后者好算但二月会出现住满 28 天交整月的争议。我一般选按实际天数并在账单备注里把起始日 / 天数 / 当月天数三个数字打出来收据上看得见争议就少一半。3.2 用 Decimal 把钱算准计费函数的最小实现from decimal import Decimal, ROUND_HALF_UP CENT Decimal(0.01) def q(x: Decimal) - Decimal: 所有金额出口统一走这个函数保证取整方向一致 return x.quantize(CENT, roundingROUND_HALF_UP) def rent_for_partial_month(month_rent: Decimal, days_used: int, days_in_month: int) - Decimal: 中途入住/退租的租金折算按当月实际天数 if days_used 0: return Decimal(0.00) if days_used days_in_month: return q(month_rent) # 住满就按整月不做除法规避 1/31 的无限小数 return q(month_rent * Decimal(days_used) / Decimal(days_in_month)) def tiered_fee(usage: Decimal, tiers): 阶梯计价tiers [(档位上限, 单价), ...]最后一档上限写 None fee, lower Decimal(0), Decimal(0) for cap, price in tiers: if usage lower: break upper Decimal(str(cap)) if cap is not None else usage seg min(usage, upper) - lower fee seg * Decimal(str(price)) lower upper return q(fee)三个函数各有各的用意。q()是唯一的取整出口把ROUND_HALF_UP收在一处比散落在十几个地方各写一遍要安全——只要有一处用了默认的银行家舍入就会出现这户差一分、那户多一分的玄学账。rent_for_partial_month()里那个days_used days_in_month的短路分支是有意加的住满整月却走除法1200 * 31 / 31在某些精度设置下不一定回到 1200.00多一次除法就多一次风险。tiered_fee()用min(usage, upper) - lower计算每档内的用电量而不是预先判断落在第几档好处是档位数可以随便改改配置不改代码。单价参数建议放在一张price_config(period, kind, tiers_json)表里按账期存快照。电价调整是常事如果只存一份当前单价去年的账单重算时会被新价覆盖对账就再也对不上了。3.3 金额大写收据上最容易被挑错的一栏收据上数字金额和大写金额不一致是最没面子的一种错。中文大写的规则不难难在零的处理1005是壹仟零伍元整1000005是壹佰万零伍元整10是壹拾元整而不是拾元整。DIGITS 零壹贰叁肆伍陆柒捌玖 UNITS [, 拾, 佰, 仟] GROUPS [, 万, 亿] def _four(n: int) - str: 0-9999 转大写内部函数不处理组间的零 s, pending_zero , False for i, ch in enumerate(str(n).zfill(4)): d, pos int(ch), 3 - i if d 0: pending_zero True else: if pending_zero and s: s 零 s DIGITS[d] UNITS[pos] pending_zero False return s def to_rmb_upper(cent: int) - str: 整数分 - 中文大写如 100500 - 壹仟零伍元整 if cent 0: return 负 to_rmb_upper(-cent) yuan, jiao, fen cent // 100, cent // 10 % 10, cent % 10 if yuan 0 and jiao 0 and fen 0: return 零元整 out if yuan: groups, y [], yuan while y 0: groups.append(y % 10000) y // 10000 for idx in range(len(groups) - 1, -1, -1): g groups[idx] if g 0: if out and not out.endswith(零): out 零 # 组内全零如 100000005 中间的亿后零 continue if out and g 1000 and not out.endswith(零): out 零 # 高位组与低位组之间的零如 壹佰万零伍 out _four(g) GROUPS[idx] out out.rstrip(零) 元 if jiao 0 and fen 0: out 整 else: if jiao: out DIGITS[jiao] 角 elif yuan: out 零 # 如 100.05 - 壹佰元零伍分 if fen: out DIGITS[fen] 分 return out拿到手先跑一组边界用例别信看着差不多输入分期望输出考察点0零元整全零1000壹拾元整十位不写拾元100500壹仟零伍元整中间单个零100000500壹佰万零伍元整跨组补零10005壹佰元零伍分角位为零、分位非零123456壹仟贰佰叁拾肆元伍角陆分完整形态提示大写函数别在打印时才调用算完账就存进bill.total_upper字段。收据、对账单、导出表格三处如果各调一次改一次规则就有三处不同步。3.4 写库前的一致性校验账单落库前的最后一道门是几条assertdef check_bill(b) - None: parts b[rent_cent] b[water_cent] b[elec_cent] b[other_cent] assert parts b[total_cent], f分项之和 {parts} ! 总额 {b[total_cent]} assert b[total_cent] 0, 出现负数总额检查是否把退款算进了本期 assert to_rmb_upper(b[total_cent]) b[total_upper], 大写与数字不一致这类断言上线时看着多余出过一次事就知道值。有个房间因为上期读数录成了 99999本期读数 1234用量算成负数账单总额直接变成负的收据上打出-XXX 元。数值校验一分钟能写完事后追查要半天。4. 套打与批量出票坐标校准、票据打印机和 PDF 留底4.1 收据模板的三种落地方式和取舍方式套打精度批量速度维护成本适合场景Word/Excel 邮件合并中靠表格边距慢几十张就卡低人人会改一次性出票套数少HTML 浏览器打印低受浏览器缩放影响中中屏幕预览为主A4 普通纸代码生成 PDF高坐标可控到 0.1mm快中改动要发版两联针式票据、需要留底走两联针式票据纸的基本只有第三条路。浏览器打印的边距、缩放、页眉页脚三个变量叠在一起同一份 HTML 在两台机器上能打出两种结果校准一次管不了多久。生成 PDF 的好处是所见即所得——你看到的 PDF 坐标就是打印机上纸的位置偏差只来自打印机本身的机械误差是个可测量的常量。4.2 用 reportlab 画一张固定坐标的收据from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import mm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont # 中文字体必须先注册否则中文全变方块这是最常见的第一次翻车 pdfmetrics.registerFont(TTFont(CN, /usr/share/fonts/truetype/noto/NotoSansSC-Regular.ttf)) PAPER (241 * mm, 140 * mm) # 两联票据纸单联尺寸按实际纸张改 OX, OY 3.5 * mm, -1.2 * mm # 全局套打偏移正值向右/向上 def draw_receipt(path: str, r: dict) - None: c canvas.Canvas(path, pagesizePAPER) c.setFont(CN, 10.5) # 针式打印机 10.5pt 是最稳的字号再小容易糊 W, H PAPER def t(x_mm, y_mm, s): c.drawString(x_mm * mm OX, H - y_mm * mm OY, s) # y 从顶部往下算便于对纸 t(18, 16, f收据号{r[serial]}) t(150, 16, f日期{r[date]}) t(18, 30, f房间{r[room_code]}) t(90, 30, f租户{r[tenant]}) t(18, 42, f房租{r[rent]:.2f}) t(90, 42, f水费{r[water]:.2f}{r[water_usage]}吨) t(18, 54, f电费{r[elec]:.2f}{r[elec_usage]}度) t(90, 54, f其他{r[other]:.2f}) t(18, 70, f合计{r[total]:.2f} 元) t(18, 86, f大写{r[total_upper]}) t(150, 100, 收款人__________) c.showPage() c.save()t()这个包装函数是为了让坐标可读——直接写H - 30*mm的话调一次位置就要在脑子里做一次减法。OY取负值是因为针式打印机的进纸定位通常比 PDF 原点偏上试出来是负的就写负的。字号钉在 10.5pt 是踩出来的针式打印机分辨率低9pt 以下汉字的横画会断成虚线12pt 以上两联纸的行距放不下五行。4.3 批量出票编号、命名、失败重试import csv, os, time, traceback def batch_output(bills, outdirout, retries2): os.makedirs(outdir, exist_okTrue) ok, fail [], [] for b in bills: fname f{b[serial]}_{b[room_code]}_{b[period]}.pdf path os.path.join(outdir, fname) for attempt in range(retries 1): try: draw_receipt(path, b) ok.append(path) break except Exception: if attempt retries: fail.append((b.get(room_code), b.get(serial), traceback.format_exc(limit2))) else: time.sleep(0.2) # 字体未加载完、磁盘瞬时占用都可能一次失败 with open(print_fail.csv, w, newline, encodingutf-8-sig) as f: csv.writer(f).writerows([(room, serial, error)] fail) return ok, fail命名里带serial是为了让归档目录按文件名排序就等于按开票顺序排事后找一张票不用翻数据库。utf-8-sig的 BOM 是给 Excel 看的否则失败清单在 Windows 上打开全是乱码。先生成 PDF 再统一送打印机而不是边算边打理由很实际批量出票时最怕打到第 17 张卡纸前面 16 张已经吐出来了后面 20 张还在不在队列里说不清。先生成、核对、再一次性打印卡纸了重打整批也不心疼。打印环节用系统命令行最省事# Linux / macOS批量送默认打印机份数 1 for f in out/*.pdf; do lp -n 1 -o mediaCustom.241x140mm $f; done # Windows用默认关联程序打印注意引号处理带空格的路径 powershell -Command Get-ChildItem out\*.pdf | ForEach-Object { Start-Process -FilePath $_.FullName -Verb Print }mediaCustom.241x140mm这个参数不能省。纸张尺寸不显式传打印机会退回 A4 缩放内容整体缩到 70%坐标全乱。Windows 那边-Verb Print走的是关联程序打印完不关进程几百张会堆一堆后台窗口批量超过 50 张建议还是走 PDF 阅读器的命令行参数。4.4 打印偏移校准三张测试页定坐标校准是个纯体力活但有固定套路把OX/OY归零用同一份模板打一张只有边框和十字线的校准页。用钢尺量四条边到纸张边缘的实际距离和设计值相减得到上下左右的偏差。取上下偏差的均值填OY左右偏差的均值填OX。注意 PDF 的 Y 轴向上为正所以内容整体偏上意味着要往负方向调。再打一张确认四个角的误差控制在 ±0.5mm 以内就算过。把这组值写进配置文件别写死在代码里——换一台打印机就得重新校一次。打印机侧设置建议值说明纸张尺寸自定义 241×140mm不要用 A4 缩放缩放比例100% / 实际大小驱动里适应页面必须关掉边距全部 0由 PDF 内部坐标控制打印质量草稿或普通高质量模式进纸速度变化会导致纵向漂移注意同一份 PDF 在两台同型号打印机上也可能有 1mm 差异属于机械公差。真要做到零误差只能一台机器一套配置按打印机名称存偏移参数。5. 避坑与排查五类错票现场的现象、成因和处置5.1 解压即用的目录选错程序起来就报错现象双击启动后提示找不到模板文件或数据库或者干脆闪退同一份程序在别人电脑上正常。原因这类免安装工具默认按相对路径找template/和data.db一旦把整个文件夹丢到C:\Program Files或者桌面某个带中文和空格的路径下写权限被系统收走或者路径解析出问题。更隐蔽的是从压缩包里直接双击运行程序实际在临时目录里展开退出就没了。解决固定解压到D:\ReceiptTool\这种纯英文无空格的短路径先完整解压再运行。如果必须放共享盘只读运行可以写数据库不行单文件数据库在网络盘上的锁机制不可靠。5.2 收据号重复或者整段跳号现象两户拿到同一个收据号或者打印中途崩溃重新打开后编号从 0050 直接跳到 0100。原因重复多半是取号用了MAX(serial)1且没有事务保护两个人同时开单读到同一个最大值。跳号则是把占号写成了预分配一批——启动时先取 50 个号放内存崩溃后这 50 个号就丢了。解决取号必须和插入收据在同一个事务里靠 UNIQUE 约束兜底冲突就重试绝不预分配号段。已经跳号的历史数据不要手工补在收据表里插一条statusvoid的占位记录说明原因比改历史安全。5.3 数字和大写差一分钱现象收据上合计 1234.56大写却是壹仟贰佰叁拾肆元伍角柒分。原因金额在中间环节被取整了两次。比如水费先round(x, 2)存成浮点出票时又格式化一次两次舍入方向不一致或者大写是由浮点金额直接乘 100 再转整数1234.56 * 100得到123455.99999999999int()截断成 123455。解决全链路只保留整数分浮点只在最外层格式化输出时出现。大写函数只接受 int 类型的分数值传 float 进来自动抛异常逼着调用方在入口就把类型理顺。5.4 换一台电脑套打位置整体偏现象同一份 PDF在办公室那台针式打印机上正正好好搬到另一台机器打出来整体偏下 4mm最后一行压到撕线。原因打印机驱动里纸张定义不一致或者适应页面没关内容被缩放了 2% 到 5%。也有可能是两台机器的纸张装订位置本来就差几毫米。解决偏移量存配置文件按打印机名做 key第一次在新机器上跑一遍 4.4 的校准流程。同时把驱动的纸张尺寸、缩放、边距三项截图存档换机器时逐项比对。5.5 单文件数据库在共享盘上损坏现象某天打开程序提示database disk image is malformed或者账单里凭空多出半条记录。原因多个客户端通过共享盘同时打开同一个 SQLite 文件写锁在网络文件系统上不生效两个进程同时写页文件结构就烂了。解决单文件数据库只允许单机单进程访问。多人开单的需求要么改成局域网内跑一个服务端哪怕是个几十行的 HTTP 接口要么按人分库、月底合并。日常备份用VACUUM INTO backup.db而不是直接拷文件拷贝过程中有写入的话拷出来的备份本身就是坏的。6. 出票前的验收用固定样例集把错票拦在打印机之外真正让我少赔笑脸的不是打印代码写得多漂亮而是一套固定样例集。做法很土造 12 个房间覆盖住满整月、月中入住、月中退租、零用水、阶梯跨档、有上期欠费、其他费用为负退押金这七种情况账期固定成三个月。每次改完计费或模板先跑这批样例把生成的 PDF 和上一次的逐字段比对再把下面这条 SQL 跑一遍。-- 三表闭合校验账单总额 - 已开票收据总额 应当等于未出票金额不为 0 就是有孤儿数据 SELECT b.period, SUM(b.total_cent) AS bill_total, SUM(CASE WHEN r.status valid THEN b.total_cent END) AS receipt_total, SUM(b.total_cent) - SUM(CASE WHEN r.statusvalid THEN b.total_cent END) AS diff, COUNT(*) AS bill_cnt FROM bill b LEFT JOIN receipt r ON r.bill_id b.id WHERE b.status void GROUP BY b.period HAVING diff 0;diff不为零有两种解释账单开了没收据正常还没送出去或者收据挂在了不存在的账单上异常外键被绕过写进去的。所以这条 SQL 的正确用法不是必须为零而是每个账期的 diff 必须等于该期statusopen的账单总额两个数一对照异常立刻现形。验收清单我压成了五行每次发版对着走一遍检查项通过标准大写金额12 组样例全对含零值和跨组补零分项合计每张账单分项之和等于总额无负数异常收据编号连续不重复作废号有占位记录套打位置首行、末行、右边界三处误差 ≤0.5mm归档完整性每张有效收据都有对应 PDF文件名含编号归档那块我吃过一次亏某月清理磁盘时按修改时间删了一批旧PDF结果里面有 6 张是补打过的收据编号对不上只能凭记忆重开。后来改成按账期/编号.pdf分层存每月出一次sha256sum清单和账单表一起打包谁动过文件一查就知道。我现在的习惯是任何跟金额、编号、坐标相关的改动先跑样例集再看对账 SQL最后才允许碰打印机。这三步顺序颠倒过一次直接打废了两联纸一沓重开还得挨个解释。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表