# -*- coding: utf-8 -*- """把报价、5 个业绩、合同关键页/发票/查验截图与劳动合同填进响应文件总册。 总册已被人工修改过,所以直接在其上增改,不重跑生成脚本;改前自动备份。 只做三件事:替换报价文本、填业绩表单元格、把 4 处「待补」占位段替换成图片块。 用法: python doc/_bid_fill_perf.py 执行 python doc/_bid_fill_perf.py --dry 只报告将做的改动 """ import copy import io import os import shutil import sys import time from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn from docx.shared import Cm, Pt sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") HERE = os.path.dirname(os.path.abspath(__file__)) BID = r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标" RESP = os.path.join( BID, "响应文件(完整版)-2026年河南产互联制造业数据智能运营平台研发项目.docx") PERF = os.path.join(HERE, "_bid_assets", "perf") OLD_CN, NEW_CN = "伍拾陆万元整", "伍拾捌万陆仟元整" OLD_NUM, NEW_NUM = "560,000.00", "586,000.00" # 竖版扫描件按 13.2cm 宽(约 18.7cm 高),保证「小标题 + 说明 + 整图 + 图注」 # 能落在同一页,不会把图挤到下一页留出大片空白 IMG_W = 13.2 INV_W = 14.4 # 发票是横向,铺满更清楚 CAP_PT = 9 # 5 个业绩:表格字段 + 合同关键页前缀 + 发票说明 PERFS = [ dict( no=1, key="c1", name="2026年河南产互联某新材料企业数字化转型改造项目", addr="河南省", amount="合同含税总额 720,500.01 元;已开票结算金额 576,400.01 元", client="河南产业互联网联合发展有限公司", date="2026 年 5 月 15 日", note="结算发票:电子发票(增值税专用发票)第 26112000002914765426 号," "开票日期 2026 年 7 月 15 日,价税合计 576,400.01 元" "(本合同约定验收后支付合同总价 80%,该笔为已结算部分)。", inv=[("i1_发票.jpg", "业绩1 电子发票(增值税专用发票)第 26112000002914765426 号" " 开票日期 2026 年 7 月 15 日 价税合计 576,400.01 元")], ), dict( no=2, key="c2", name="智能制造平台(数字化采购执行业务蓝图)委托开发项目" "(合同编号 2024004)", addr="天津市", amount="339,196.00 元(含税,已全额开票结算)", client="智盛易(天津)科技有限公司", date="2024 年 7 月 10 日", note="结算发票:电子发票(增值税专用发票)第 24112000000220367465 号," "开票日期 2024 年 12 月 4 日,价税合计 339,196.00 元。", inv=[("i2_发票.jpg", "业绩2 电子发票(增值税专用发票)第 24112000000220367465 号" " 开票日期 2024 年 12 月 4 日 价税合计 339,196.00 元")], ), dict( no=3, key="c3", name="智能制造平台(数字化产销协同业务蓝图)委托开发项目" "(合同编号 2024001)", addr="天津市", amount="318,000.00 元(含税,已全额开票结算)", client="智盛易(天津)科技有限公司", date="2024 年 1 月 5 日", note="结算发票:增值税专用发票 4 张,发票代码 1100233130,发票号码 " "26271801、26271802、26271803(各 100,000.00 元)与 26271804" "(18,000.00 元),开票日期 2024 年 2 月 6 日,价税合计合计 " "318,000.00 元。", inv=[("i3_发票1.jpg", "业绩3 增值税专用发票 No.26271803 " "开票日期 2024 年 2 月 6 日 价税合计 100,000.00 元"), ("i3_发票2.jpg", "业绩3 增值税专用发票 No.26271802 " "开票日期 2024 年 2 月 6 日 价税合计 100,000.00 元"), ("i3_发票3.jpg", "业绩3 增值税专用发票 No.26271801 " "开票日期 2024 年 2 月 6 日 价税合计 100,000.00 元"), ("i3_发票4.jpg", "业绩3 增值税专用发票 No.26271804 " "开票日期 2024 年 2 月 6 日 价税合计 18,000.00 元")], ), dict( no=4, key="c4", name="智能制造平台(数字化供应协同业务蓝图)委托开发项目" "(合同编号 2024003)", addr="天津市", amount="233,200.00 元(含税,已全额开票结算)", client="智盛易(天津)科技有限公司", date="2024 年 5 月 8 日", note="结算发票:电子发票(增值税专用发票)第 24112000000142493545 号," "开票日期 2024 年 9 月 9 日,价税合计 233,200.00 元。", inv=[("i4_发票.jpg", "业绩4 电子发票(增值税专用发票)第 24112000000142493545 号" " 开票日期 2024 年 9 月 9 日 价税合计 233,200.00 元")], ), dict( no=5, key="c5", name="智能制造平台(数字化生产执行业务蓝图)委托开发项目" "(合同编号 2025002)", addr="天津市", amount="200,000.00 元(含税,已全额开票结算)", client="智盛易(天津)科技有限公司", date="2025 年 5 月 9 日", note="结算发票:电子发票(增值税专用发票)第 25112000000135255646 号," "开票日期 2025 年 6 月 10 日,价税合计 200,000.00 元。", inv=[("i5_发票.jpg", "业绩5 电子发票(增值税专用发票)第 25112000000135255646 号" " 开票日期 2025 年 6 月 10 日 价税合计 200,000.00 元")], ), ] CON_PAGES = [("首页", "合同首页"), ("服务内容页", "合同服务内容页"), ("金额页", "合同金额页"), ("签字盖章页", "合同签字盖章页")] # 已有的税务系统查验截图(仅业绩3 的 4 张纸质专票) VERIFIED = [("v3_查验1.jpg", "业绩3 增值税专用发票 No.26271802 查验结果" "(价税合计 100,000.00 元)"), ("v3_查验2.jpg", "业绩3 增值税专用发票 No.26271804 查验结果" "(价税合计 18,000.00 元)"), ("v3_查验3.jpg", "业绩3 增值税专用发票 No.26271801 查验结果" "(价税合计 100,000.00 元)"), ("v3_查验4.jpg", "业绩3 增值税专用发票 No.26271803 查验结果" "(价税合计 100,000.00 元)")] PENDING_VERIFY = [ (1, "26112000002914765426", "2026 年 7 月 15 日", "576,400.01"), (2, "24112000000220367465", "2024 年 12 月 4 日", "339,196.00"), (4, "24112000000142493545", "2024 年 9 月 9 日", "233,200.00"), (5, "25112000000135255646", "2025 年 6 月 10 日", "200,000.00"), ] LABOR = [(1, "卿劢", "数字化供应链总监", "2024 年 1 月 5 日至 2027 年 1 月 4 日", 6), (7, "郭峰", "开发总监", "2026 年 7 月 17 日至 2029 年 7 月 16 日", 2)] def clone_pPr(src): return copy.deepcopy(src._p.pPr) if src._p.pPr is not None else None def clone_rPr(src): """取原文段落首个 run 的字符格式,去掉高亮/底纹/加粗等装饰。""" for r in src.runs: if r._element.rPr is None: continue rPr = copy.deepcopy(r._element.rPr) for tag in ("w:highlight", "w:shd", "w:b", "w:bCs", "w:i", "w:iCs", "w:u", "w:color"): for e in rPr.findall(qn(tag)): rPr.remove(e) return rPr return None def drop_indent(p): """去掉首行缩进(含按字符计的 firstLineChars,否则 Word 里仍会缩进)。""" ind = p._p.get_or_add_pPr().find(qn("w:ind")) if ind is None: return for a in ("w:firstLine", "w:firstLineChars", "w:hanging", "w:hangingChars"): if ind.get(qn(a)) is not None: del ind.attrib[qn(a)] def relax_row_height(row): """把固定行高改为「最小值」,避免长文本被截断。""" trPr = row._tr.get_or_add_trPr() for h in trPr.findall(qn("w:trHeight")): h.set(qn("w:hRule"), "atLeast") def put_cell(cell, text): """只改文字,保留单元格原有字体与段落格式。""" p = cell.paragraphs[0] if p.runs: p.runs[0].text = text for r in p.runs[1:]: r.text = "" else: p.add_run(text) class Block: """在占位段之前依次插入内容,最后删掉占位段。 所有新段落的段落格式与字符格式都从占位段(同章节原文)克隆而来, 只去掉高亮,保证缩进、行距、中西文字体与上下文完全一致。 """ def __init__(self, anchor): self.a = anchor self.pPr = clone_pPr(anchor) self.rPr = clone_rPr(anchor) self.n_img = 0 def _new(self, text=""): p = self.a.insert_paragraph_before("") if self.pPr is not None: p._p.insert(0, copy.deepcopy(self.pPr)) r = p.add_run(text) if self.rPr is not None: r._element.insert(0, copy.deepcopy(self.rPr)) return p, r def head(self, text): p, r = self._new(text) r.font.bold = True p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(2) p.paragraph_format.keep_with_next = True return p def text(self, text): p, _ = self._new(text) p.paragraph_format.space_after = Pt(2) return p def img(self, path, caption, width=IMG_W): p, r = self._new() # 图片段落必须单倍行距:正文是 1.5 倍,套到整页扫描件上会凭空多出 # 一半高度,直接把图挤到下一页 p.paragraph_format.line_spacing = 1.0 p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(6) p.paragraph_format.space_after = Pt(2) p.paragraph_format.keep_with_next = True drop_indent(p) r.add_picture(path, width=Cm(width)) c, cr = self._new(caption) c.paragraph_format.line_spacing = 1.0 c.alignment = WD_ALIGN_PARAGRAPH.CENTER c.paragraph_format.space_after = Pt(8) drop_indent(c) cr.font.size = Pt(CAP_PT) self.n_img += 1 def done(self): self.a._p.getparent().remove(self.a._p) def find_placeholder(doc, needle, nth=0): hits = [p for p in doc.paragraphs if needle in p.text] return hits[nth] def main(): dry = "--dry" in sys.argv if not os.path.isdir(PERF): raise SystemExit(f"缺少素材目录 {PERF}(先跑 _bid_prep_perf.py)") if not dry: bak = RESP[:-5] + f"_改价与业绩前备份_{time.strftime('%Y%m%d_%H%M')}.docx" shutil.copy2(RESP, bak) print(f"已备份 → {os.path.basename(bak)}\n") doc = Document(RESP) # ---- 1 报价 n = 0 for p in doc.paragraphs: for r in p.runs: if OLD_CN in r.text or OLD_NUM in r.text: r.text = r.text.replace(OLD_CN, NEW_CN).replace(OLD_NUM, NEW_NUM) n += 1 for row in doc.tables[0].rows: for cell in row.cells: for p in cell.paragraphs: for r in p.runs: if OLD_CN in r.text or OLD_NUM in r.text: r.text = (r.text.replace(OLD_CN, NEW_CN) .replace(OLD_NUM, NEW_NUM)) n += 1 print(f"[1] 报价:改写 {n} 处 run → {NEW_CN} / ¥{NEW_NUM}") # ---- 2 业绩表(表2~表6 依次对应业绩1~5) for i, d in enumerate(PERFS): t = doc.tables[2 + i] vals = [d["name"], d["addr"], d["amount"], d["client"], d["date"], d["note"]] for row, v in zip(t.rows, vals): relax_row_height(row) put_cell(row.cells[1], v) print(f"[2] 业绩{d['no']} 表格已填:{d['name'][:30]}…") # ---- 3.1 合同关键页 b = Block(find_placeholder(doc, "【待补:本项扫描件由供应商提供后插入】", 0)) for d in PERFS: b.head(f"业绩{d['no']} {d['name']}") b.text(f"委托人:{d['client']} 合同签订日期:{d['date']} " f"合同金额:{d['amount']}") for tag, cap in CON_PAGES: b.img(os.path.join(PERF, f"{d['key']}_{tag}.jpg"), f"业绩{d['no']} {cap}") b.done() print(f"[3.1] 合同关键页:插入 {b.n_img} 页") # ---- 3.2 结算发票 b = Block(find_placeholder(doc, "【待补:本项扫描件由供应商提供后插入】", 0)) for d in PERFS: b.head(f"业绩{d['no']} 结算发票") b.text(d["note"]) for fn, cap in d["inv"]: b.img(os.path.join(PERF, fn), cap, width=INV_W) b.done() print(f"[3.2] 结算发票:插入 {b.n_img} 张") # ---- 3.3 发票查验截图 b = Block(find_placeholder(doc, "【待补:本项扫描件由供应商提供后插入】", 0)) b.head("业绩3 增值税专用发票网上税务系统查验结果") b.text("以下为业绩3 对应 4 张增值税专用发票在国家税务总局全国增值税发票查验平台" "(https://inv-veri.chinatax.gov.cn/)的查验结果," "发票代码 1100233130,开票日期均为 2024 年 2 月 6 日," "价税合计合计 318,000.00 元。") for fn, cap in VERIFIED: b.img(os.path.join(PERF, fn), cap, width=INV_W) b.head("业绩1、2、4、5 电子发票查验结果") b.text("上述四张电子发票(增值税专用发票)的查验参数如下,查验结果截图待补:") for no, code, date, amt in PENDING_VERIFY: b.text(f"  业绩{no}:发票号码 {code},开票日期 {date}," f"价税合计 {amt} 元。") b.text("【待补:上述 4 张电子发票在 https://inv-veri.chinatax.gov.cn/ " "的查验结果截图】") b.done() print(f"[3.3] 查验截图:插入 {b.n_img} 张,另留 4 张待补") # ---- 劳动合同 b = Block(find_placeholder(doc, "【待补:上表各岗位人员姓名及与我方签署的劳务合同")) b.head("项目团队人员劳动合同") b.text("我方与项目团队人员签订的劳动合同扫描件如下。") for start, name, post, term, cnt in LABOR: b.head(f"{name} {post} 合同期限 {term}") for k in range(cnt): b.img(os.path.join(PERF, f"lab_p{start + k}.jpg"), f"{name} 劳动合同第 {k + 1} 页") b.done() print(f"[4] 劳动合同:插入 {b.n_img} 页") if dry: print("\n--dry:未写回文档") return doc.save(RESP) mb = os.path.getsize(RESP) / 1024 / 1024 print(f"\n已写回 {os.path.basename(RESP)} {mb:.1f}MB") if __name__ == "__main__": main()