检测机构月底关账,发票台账上报告已出未开票怎么自动对出来
月底关账那几天财务在群里问的是同一句话这个月报告出了、票还没开的一共多少这背后是三张表——客服的开票申请单、报告完成台账、财务的发票台账各自在不同人手里对起来只能一单单翻。翻表只是累真容易出事的是判断报告出了没有抬头、税号填全了吗必须凭 PO 的客户PO 号给了吗申请金额跟台账算得一样吗这一单是不是已经开过票为了解决这个问题Python 提供了 openpyxl读三张 CSV 表把这些判定写成代码再把结果写回一张 Excel 台账。不用另装数据库也不碰开票系统——它只告诉你这批能开哪几张、剩下的卡在哪一步。本文按四步走配置路径与字段 → 读三张表归堆 → 逐单判够不够格 → 出清单和对账台账代码取自配套示例的run.py。一、开票卡的不是开是够不够格开票本身是在开票软件里点几下的事。麻烦在点之前单子由客服一单一单递过来报告出没出、开过没有没人说得准。口径还按客户分有的按委托单逐张开有的按月汇总一张。作废票最容易形成死结上月开错一张、红冲后要重开一到「已经开过票」就被拦住。第一条规矩就是——只看状态正常的票作废票既不占额度、也不挡重开。二、第一步把路径和字段写在一个地方出处run.py顶部的路径与字段常量区REQ_CSV RAW_DIR / 开票申请单.csv # 客服/业务交上来的开票申请 REP_CSV RAW_DIR / 报告完成台账.csv # 已出报告出了报告才有开票依据 INV_CSV RAW_DIR / 已开发票台账.csv # 财务已经开出去的票含作废 RULE_CSV RAW_DIR / 客户开票规则表.csv # 客户 → 开票方式 / 税率 / 结算周期 / 是否必须 PO TPL_XLSX TPL_DIR / 开票对账台账模板.xlsx LEDGER_XLSX OUT_DIR / 开票对账台账.xlsx IMPORT_CSV OUT_DIR / 开票数据_导入用.csv # 给开票软件导入用出处run.py顶部的路径与字段常量区判空列那一段REQ_HEAD 申请日期 REP_HEAD 委托单号 INV_HEAD 发票号码 RULE_HEAD 客户名称路径都从Path(__file__).resolve().parent往下接换机器不用改。申请单天天变规则表几个月才动一次四张表分开放改口径不用碰代码。判空列那四行看着多余其实是整段脚本里最要紧的一处防护。三、第二步读三张表先归堆出处run.py的read_csv_rows()在norm()后面def read_csv_rows(path, head_col): 读一张 CSV 成 dict 列表全部按文本取。 **一律不转类型**——委托单号 WT2026-0000715、发票号 00012301 一转 int 前导零就没了 跟开票软件里的单号对不上。表尾的说明行靠 head_col 那一列为空跳过所以说明文字别写进那一列。 rows [] with open(path, r, encodingutf-8-sig, newline) as f: for raw in csv.DictReader(f): if not raw or not norm(raw.get(head_col, )): continue rows.append({k: ( if v is None else str(v)) for k, v in raw.items()}) return rows为什么要单独传head_col表尾说明靠「哪一列一定有值」跳过。给客户规则表判空时顺手用「申请日期」那张表根本没这一列——每行取出来都是空整张规则表被读成空的所有单子都挂「客户没进规则表」。换表先问靠哪一列判空。出处run.py的to_money()与tax_rate()紧跟norm()def to_money(text): 把表里的金额文本转成两位小数。带千分位逗号和 的照样读——系统导出来就长这样。 s str(text).strip().replace(,, ).replace(, ).replace(¥, ) if not s: return 0.0 return round(float(s), 2) def tax_rate(text): 税率写成「6%」也能读——导出表里常带百分号直接 float 会炸。 return round(float(str(text).strip().replace(%, )) / 100, 4)金额带千分位逗号、税率带百分号是导出表的常态两个float()都会抛异常先洗再转空值当 0。出处run.py的read_rules()在read_invoices()后面def read_rules(): 客户开票规则表客户 → 开票方式 / 税率 / 结算周期 / 是否必须 PO。 客户名当键时压成去全角空格的比较键显示名另外存一份干净的—— 同一家客户在申请单里多打一个空格本来会被当成两家。 rules {} for row in read_csv_rows(RULE_CSV, RULE_HEAD): rules[norm(row[客户名称])] { 客户名称: BLANK_RE.sub(, row[客户名称].strip()), 开票方式: row[开票方式].strip(), 税率: row[税率].strip(), 结算周期: row[结算周期].strip(), 是否必须PO: row[是否必须PO].strip(), } return rules客户名当键要归一化norm()去掉所有空白含全角空格strip()不管它。规则表里名字带个全角空格、申请单里不带不归一化就是两家客户两边单子全挂。别上模糊匹配——这行的客户名本来就长得很像。出处run.py的contract_used()紧跟read_rules()def contract_used(invoices, reports): 各合同已经占掉的额度不含税只算状态正常的票作废票不占。 used {} for inv in invoices: if inv[状态].strip() ! 正常: continue rep reports.get(norm(inv[关联委托单])) if rep: key norm(rep[合同号]) used[key] used.get(key, 0.0) to_money(rep[不含税金额]) return used额度按不含税累计拿价税合计去比会提前报超作废票在这里也跳过否则额度被白占。四、第三步逐单判够不够格出处run.py的plan_one()文件中部资料与金额两组判定cust norm(req[客户名称]) if cust not in rules: return None, 客户规则, 这家客户没进开票规则表先定开票方式与税率再开 rule rules[cust] no norm(req[委托单号]) if not req[发票抬头].strip() or not req[纳税人识别号].strip(): return None, 开票信息, 发票抬头或纳税人识别号空着开票软件这一步就过不去 rep reports.get(no) if rep is None: # 出了报告才有开票依据 return None, 报告未出, 报告完成台账里没有这一单报告没出不能先开票 if rule[是否必须PO] 是 and not req[PO号].strip(): return None, 缺PO, f这家客户必须凭 PO 开票申请单上 PO 号空着 tax tax_rate(rule[税率]) net to_money(rep[不含税金额]) due round(net * (1 tax), 2) # 申请金额应当等于 不含税 ×1税率 want to_money(req[申请开票金额]) if abs(want - due) 0.005: return None, 金额不符, f申请金额 {want:,.2f}台账算出来是 {due:,.2f}差 {abs(want - due):,.2f}出处run.py的plan_one()文件中部重票与额度那一组判定if no in opened: # 作废票不算开过重开要走到这里 return None, 重复开票, f这一单已经开过票{opened[no]}再开一次就是重票 key norm(rep[合同号]) cap to_money(rep[合同额]) used used_net.get(key, 0.0) if used net cap 0.005: return None, 超合同额度, (f合同 {key} 额度 {cap:,.2f}不含税已占 {used:,.2f} f这一单 {net:,.2f} 会超)判定顺序就是业务顺序这是唯一不能自己发挥的地方判定按顺序卡住什么委托单号单号空着和开票软件里的单子对不上客户规则新客户没定开票方式与税率先定再开开票信息抬头、纳税人识别号空着开票软件过不去报告未出报告完成台账里没有这一单不能先开票缺PO必须凭 PO 的客户 PO 号没给开出去客户不认金额不符申请金额与台账算出来的差多少当场指出来重复开票已开过正常票再开一次就是重票超合同额度不含税额度不够得先签补充协议顺序反了理由就指向错误的动作把额度排在报告前面客服拿着「额度不够」去签补充协议回来才发现这单报告还在审核。「重复开票」放后面也有讲究——前面全通过了才轮到它。五、第四步出清单再算关账差额可开票的进第一张表挂起的进第二张第三张是按客户的关账对账。出处run.py的build_recon()文件后半先算已开、再汇总opened {} for inv in invoices: if inv[状态].strip() ! 正常: continue rep reports.get(norm(inv[关联委托单])) if rep: opened[norm(inv[关联委托单])] to_money(rep[不含税金额]) names, total, invo, cnt {}, {}, {}, {} for rep in reports.values(): key norm(rep[客户名称]) names[key] BLANK_RE.sub(, rep[客户名称].strip()) net to_money(rep[不含税金额]) total[key] total.get(key, 0.0) net if norm(rep[委托单号]) not in opened: cnt[key] cnt.get(key, 0) 1 for no_ in opened: rep reports[no_] key norm(rep[客户名称]) invo[key] invo.get(key, 0.0) opened[no_]作废票三处都要跳过额度不占、重票判断不挡重开、已开金额不算已开。第三处漏了差额就偏小。出处run.py的write_ledger()文件后半第一张表那一段ws wb[可开票清单] for item in items: ws.append([item[委托单号], item[客户名称], item[合同号], item[PO号], item[不含税金额], item[税率], item[税额], item[价税合计], item[发票抬头], item[纳税人识别号], item[开票方式], item[结算周期]]) for c in ws[ws.max_row]: c.font Font(name微软雅黑, size10) c.border border if c.column 1: c.number_format # 委托单号带前导零当文本存 c.alignment Alignment(horizontalleft) elif c.column in (5, 7, 8): c.number_format #,##0.00委托单号那列必须设成文本格式一旦有人转成数字前导零没了跟开票软件里的单子对不上。另外还出一份02_output/开票数据_导入用.csv给开票软件导入用。总结一先定判断再谈开票能不能开按业务顺序过一遍七项判定挂起理由指名缺什么、差多少。二作废票要三处都跳过不占额度、不挡重开、不算已开金额第三处漏了关账差额就偏小。三口径按客户走开票方式、税率、是否凭 PO、合同不含税额度都在规则表里换客户改表不改代码。示例回读 34 项全绿换掉01_raw_data/里的四张表就能直接用。完整源码本文配套的可运行示例已开源换掉01_raw_data/里的申请单、报告台账、已开发票台账和客户规则表就能直接复跑huang_jianhua0101/examples - Gitee.com关于我在实验室一线待了 13 年9 年制药 4 年第三方检测做的一直是实验室信息化。做过 STARLIMS 的甲方 PM——一期、二期两轮上线都由我主导招标到 3Q 验证到验收全流程也在系统上自己做过二次开发——把纸质的账号申请流程搬到线上跑在 STARLIMS 之前还有 6 年多 CS 架构 LIMS 的使用与运维经验其中一段经 Citrix 远程接入。现在专做实验室里那些重复劳动报表自动生成、仪器数据对接、合规文档批量处理。本科物理化学、硕士计算机化学既听得懂 QA 说的变更控制也看得懂仪器导出的原始数据长什么样。SOP、偏差、OOS、样本流转这些词不用你解释。现在主要做这几类- 检验报告与台账批量生成模板不动数据自动填格式一步不错- 仪器数据对接色谱、光谱、酶标仪导出的原始文件解析、清洗、入库、转成报表- 合规文档自动化SOP、验证方案、批记录这类重复文档的批量生成与核对- 数据完整性核查按 ALCOA 逐条核对原始数据与记录是否对得上手里有这类活儿卡着或者只是想问问能不能自动化都欢迎评论区聊先把问题说清楚再谈怎么做。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →