用 openpyxl 导出可复算预算表:从单项小计到跨 Sheet 汇总
预算工作簿常常有“数量、单价、单项小计”三列。生成文件时三个数都正确,并不代表这个工作簿可以用于后续调整:如果小计写的是数字常量,用户修改数量或单价,表中原来的总额就不会变化。这类问题在导出当下不容易察觉,通常要到活动规模或供应商报价变化时才暴露。
我在一个公开的校园活动预算脚本中复现了这种情况。它导出的第一条费用是数量 1、单价 800、小计 800,整份预算打印 7927 元。把数量从 1 改成 2 后,小计仍为 800,分类小计、总合计与第二张汇总表也保持原值。用 openpyxl 读取原文件,G2.data_type 是 n,说明这里存储的是数字,而非公式。该问题已作为可复现的项目 Issue记录;下面讨论通用做法,不涉及账号数据。
先确定工作簿的用途
静态报表和可编辑预算是两种不同产品。如果只需要一次性归档,固定数值可以接受,但应在工作簿里明确标注“修改单元格不会自动重新计算”。如果使用者会修改数量、单价或费率,就应把各级计算关系保存在工作簿中。仅在 Python 里求和、再把结果写成常量,会使工作簿在导出后失去计算链。
让明细和汇总引用单元格
假设数量在 E 列、单价在 F 列、小计在 G 列,两条明细占第 2、3 行,示意代码如下:
from openpyxl import Workbook
from openpyxl.workbook.properties import CalcProperties
wb = Workbook()
detail = wb.active
detail.title = "预算明细"
detail["E2"], detail["F2"] = 1, 800
detail["E3"], detail["F3"] = 2, 200
for row in (2, 3):
detail[f"G{row}"] = f"=E{row}*F{row}"
detail["G4"] = "=SUM(G2:G3)" # 基础费用
detail["G5"] = "=ROUND(G4*8%,0)" # 示例备用金;实际费率应与表头一致
detail["G6"] = "=G4+G5" # 总额
summary = wb.create_sheet("预算汇总")
summary["B2"] = "='预算明细'!G4"
summary["B3"] = "='预算明细'!G5"
summary["B4"] = "='预算明细'!G6"
wb.calculation = CalcProperties(calcMode="auto", fullCalcOnLoad=True)
wb.save("budget.xlsx")
这样,明细小计依赖数量与单价,备用金依赖基础费用,总额再引用两者,第二张表也引用明细表中的计算结果。分类小计可使用各分类对应行的 SUM,或者使用分类列的 SUMIF;如果明细行可能排序、插入或删除,要特别检查引用范围是否仍正确。不要让表头写 5%,公式却计算 8%。
验收时分清公式存在与公式结果正确
openpyxl 会写入和读取公式,但它本身不计算公式。生成文件后,可以先以 data_only=False 重开,断言 G2 是 =E2*F2、汇总单元格引用了正确的行;这验证的是公式连接关系。要验证最终显示金额,还应在会计算的表格软件中打开文件,修改一个代表性数量并确认小计、分类小计、备用金、总额和汇总表全部变化。对无法控制表格软件的自动化环境,不应把写入公式等同于验证计算结果。
最后,最好增加一条“逆向检查”:把生成工作簿的某个数量翻倍,预期受影响的值也发生相应变化,而无关分类保持原值。这个检查比只对比导出时的总金额更容易抓住静态常量造成的失效,也能及早发现跨 Sheet 引用漏接。
- 点赞
- 收藏
- 关注作者
评论(0)