_bid_fill_perf.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. # -*- coding: utf-8 -*-
  2. """把报价、5 个业绩、合同关键页/发票/查验截图与劳动合同填进响应文件总册。
  3. 总册已被人工修改过,所以直接在其上增改,不重跑生成脚本;改前自动备份。
  4. 只做三件事:替换报价文本、填业绩表单元格、把 4 处「待补」占位段替换成图片块。
  5. 用法:
  6. python doc/_bid_fill_perf.py 执行
  7. python doc/_bid_fill_perf.py --dry 只报告将做的改动
  8. """
  9. import copy
  10. import io
  11. import os
  12. import shutil
  13. import sys
  14. import time
  15. from docx import Document
  16. from docx.enum.text import WD_ALIGN_PARAGRAPH
  17. from docx.oxml.ns import qn
  18. from docx.shared import Cm, Pt
  19. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
  20. HERE = os.path.dirname(os.path.abspath(__file__))
  21. BID = r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标"
  22. RESP = os.path.join(
  23. BID, "响应文件(完整版)-2026年河南产互联制造业数据智能运营平台研发项目.docx")
  24. PERF = os.path.join(HERE, "_bid_assets", "perf")
  25. OLD_CN, NEW_CN = "伍拾陆万元整", "伍拾捌万陆仟元整"
  26. OLD_NUM, NEW_NUM = "560,000.00", "586,000.00"
  27. # 竖版扫描件按 13.2cm 宽(约 18.7cm 高),保证「小标题 + 说明 + 整图 + 图注」
  28. # 能落在同一页,不会把图挤到下一页留出大片空白
  29. IMG_W = 13.2
  30. INV_W = 14.4 # 发票是横向,铺满更清楚
  31. CAP_PT = 9
  32. # 5 个业绩:表格字段 + 合同关键页前缀 + 发票说明
  33. PERFS = [
  34. dict(
  35. no=1, key="c1",
  36. name="2026年河南产互联某新材料企业数字化转型改造项目",
  37. addr="河南省",
  38. amount="合同含税总额 720,500.01 元;已开票结算金额 576,400.01 元",
  39. client="河南产业互联网联合发展有限公司",
  40. date="2026 年 5 月 15 日",
  41. note="结算发票:电子发票(增值税专用发票)第 26112000002914765426 号,"
  42. "开票日期 2026 年 7 月 15 日,价税合计 576,400.01 元"
  43. "(本合同约定验收后支付合同总价 80%,该笔为已结算部分)。",
  44. inv=[("i1_发票.jpg",
  45. "业绩1 电子发票(增值税专用发票)第 26112000002914765426 号"
  46. " 开票日期 2026 年 7 月 15 日 价税合计 576,400.01 元")],
  47. ),
  48. dict(
  49. no=2, key="c2",
  50. name="智能制造平台(数字化采购执行业务蓝图)委托开发项目"
  51. "(合同编号 2024004)",
  52. addr="天津市",
  53. amount="339,196.00 元(含税,已全额开票结算)",
  54. client="智盛易(天津)科技有限公司",
  55. date="2024 年 7 月 10 日",
  56. note="结算发票:电子发票(增值税专用发票)第 24112000000220367465 号,"
  57. "开票日期 2024 年 12 月 4 日,价税合计 339,196.00 元。",
  58. inv=[("i2_发票.jpg",
  59. "业绩2 电子发票(增值税专用发票)第 24112000000220367465 号"
  60. " 开票日期 2024 年 12 月 4 日 价税合计 339,196.00 元")],
  61. ),
  62. dict(
  63. no=3, key="c3",
  64. name="智能制造平台(数字化产销协同业务蓝图)委托开发项目"
  65. "(合同编号 2024001)",
  66. addr="天津市",
  67. amount="318,000.00 元(含税,已全额开票结算)",
  68. client="智盛易(天津)科技有限公司",
  69. date="2024 年 1 月 5 日",
  70. note="结算发票:增值税专用发票 4 张,发票代码 1100233130,发票号码 "
  71. "26271801、26271802、26271803(各 100,000.00 元)与 26271804"
  72. "(18,000.00 元),开票日期 2024 年 2 月 6 日,价税合计合计 "
  73. "318,000.00 元。",
  74. inv=[("i3_发票1.jpg", "业绩3 增值税专用发票 No.26271803 "
  75. "开票日期 2024 年 2 月 6 日 价税合计 100,000.00 元"),
  76. ("i3_发票2.jpg", "业绩3 增值税专用发票 No.26271802 "
  77. "开票日期 2024 年 2 月 6 日 价税合计 100,000.00 元"),
  78. ("i3_发票3.jpg", "业绩3 增值税专用发票 No.26271801 "
  79. "开票日期 2024 年 2 月 6 日 价税合计 100,000.00 元"),
  80. ("i3_发票4.jpg", "业绩3 增值税专用发票 No.26271804 "
  81. "开票日期 2024 年 2 月 6 日 价税合计 18,000.00 元")],
  82. ),
  83. dict(
  84. no=4, key="c4",
  85. name="智能制造平台(数字化供应协同业务蓝图)委托开发项目"
  86. "(合同编号 2024003)",
  87. addr="天津市",
  88. amount="233,200.00 元(含税,已全额开票结算)",
  89. client="智盛易(天津)科技有限公司",
  90. date="2024 年 5 月 8 日",
  91. note="结算发票:电子发票(增值税专用发票)第 24112000000142493545 号,"
  92. "开票日期 2024 年 9 月 9 日,价税合计 233,200.00 元。",
  93. inv=[("i4_发票.jpg",
  94. "业绩4 电子发票(增值税专用发票)第 24112000000142493545 号"
  95. " 开票日期 2024 年 9 月 9 日 价税合计 233,200.00 元")],
  96. ),
  97. dict(
  98. no=5, key="c5",
  99. name="智能制造平台(数字化生产执行业务蓝图)委托开发项目"
  100. "(合同编号 2025002)",
  101. addr="天津市",
  102. amount="200,000.00 元(含税,已全额开票结算)",
  103. client="智盛易(天津)科技有限公司",
  104. date="2025 年 5 月 9 日",
  105. note="结算发票:电子发票(增值税专用发票)第 25112000000135255646 号,"
  106. "开票日期 2025 年 6 月 10 日,价税合计 200,000.00 元。",
  107. inv=[("i5_发票.jpg",
  108. "业绩5 电子发票(增值税专用发票)第 25112000000135255646 号"
  109. " 开票日期 2025 年 6 月 10 日 价税合计 200,000.00 元")],
  110. ),
  111. ]
  112. CON_PAGES = [("首页", "合同首页"), ("服务内容页", "合同服务内容页"),
  113. ("金额页", "合同金额页"), ("签字盖章页", "合同签字盖章页")]
  114. # 已有的税务系统查验截图(仅业绩3 的 4 张纸质专票)
  115. VERIFIED = [("v3_查验1.jpg", "业绩3 增值税专用发票 No.26271802 查验结果"
  116. "(价税合计 100,000.00 元)"),
  117. ("v3_查验2.jpg", "业绩3 增值税专用发票 No.26271804 查验结果"
  118. "(价税合计 18,000.00 元)"),
  119. ("v3_查验3.jpg", "业绩3 增值税专用发票 No.26271801 查验结果"
  120. "(价税合计 100,000.00 元)"),
  121. ("v3_查验4.jpg", "业绩3 增值税专用发票 No.26271803 查验结果"
  122. "(价税合计 100,000.00 元)")]
  123. PENDING_VERIFY = [
  124. (1, "26112000002914765426", "2026 年 7 月 15 日", "576,400.01"),
  125. (2, "24112000000220367465", "2024 年 12 月 4 日", "339,196.00"),
  126. (4, "24112000000142493545", "2024 年 9 月 9 日", "233,200.00"),
  127. (5, "25112000000135255646", "2025 年 6 月 10 日", "200,000.00"),
  128. ]
  129. LABOR = [(1, "卿劢", "数字化供应链总监", "2024 年 1 月 5 日至 2027 年 1 月 4 日", 6),
  130. (7, "郭峰", "开发总监", "2026 年 7 月 17 日至 2029 年 7 月 16 日", 2)]
  131. def clone_pPr(src):
  132. return copy.deepcopy(src._p.pPr) if src._p.pPr is not None else None
  133. def clone_rPr(src):
  134. """取原文段落首个 run 的字符格式,去掉高亮/底纹/加粗等装饰。"""
  135. for r in src.runs:
  136. if r._element.rPr is None:
  137. continue
  138. rPr = copy.deepcopy(r._element.rPr)
  139. for tag in ("w:highlight", "w:shd", "w:b", "w:bCs", "w:i", "w:iCs",
  140. "w:u", "w:color"):
  141. for e in rPr.findall(qn(tag)):
  142. rPr.remove(e)
  143. return rPr
  144. return None
  145. def drop_indent(p):
  146. """去掉首行缩进(含按字符计的 firstLineChars,否则 Word 里仍会缩进)。"""
  147. ind = p._p.get_or_add_pPr().find(qn("w:ind"))
  148. if ind is None:
  149. return
  150. for a in ("w:firstLine", "w:firstLineChars", "w:hanging", "w:hangingChars"):
  151. if ind.get(qn(a)) is not None:
  152. del ind.attrib[qn(a)]
  153. def relax_row_height(row):
  154. """把固定行高改为「最小值」,避免长文本被截断。"""
  155. trPr = row._tr.get_or_add_trPr()
  156. for h in trPr.findall(qn("w:trHeight")):
  157. h.set(qn("w:hRule"), "atLeast")
  158. def put_cell(cell, text):
  159. """只改文字,保留单元格原有字体与段落格式。"""
  160. p = cell.paragraphs[0]
  161. if p.runs:
  162. p.runs[0].text = text
  163. for r in p.runs[1:]:
  164. r.text = ""
  165. else:
  166. p.add_run(text)
  167. class Block:
  168. """在占位段之前依次插入内容,最后删掉占位段。
  169. 所有新段落的段落格式与字符格式都从占位段(同章节原文)克隆而来,
  170. 只去掉高亮,保证缩进、行距、中西文字体与上下文完全一致。
  171. """
  172. def __init__(self, anchor):
  173. self.a = anchor
  174. self.pPr = clone_pPr(anchor)
  175. self.rPr = clone_rPr(anchor)
  176. self.n_img = 0
  177. def _new(self, text=""):
  178. p = self.a.insert_paragraph_before("")
  179. if self.pPr is not None:
  180. p._p.insert(0, copy.deepcopy(self.pPr))
  181. r = p.add_run(text)
  182. if self.rPr is not None:
  183. r._element.insert(0, copy.deepcopy(self.rPr))
  184. return p, r
  185. def head(self, text):
  186. p, r = self._new(text)
  187. r.font.bold = True
  188. p.paragraph_format.space_before = Pt(10)
  189. p.paragraph_format.space_after = Pt(2)
  190. p.paragraph_format.keep_with_next = True
  191. return p
  192. def text(self, text):
  193. p, _ = self._new(text)
  194. p.paragraph_format.space_after = Pt(2)
  195. return p
  196. def img(self, path, caption, width=IMG_W):
  197. p, r = self._new()
  198. # 图片段落必须单倍行距:正文是 1.5 倍,套到整页扫描件上会凭空多出
  199. # 一半高度,直接把图挤到下一页
  200. p.paragraph_format.line_spacing = 1.0
  201. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  202. p.paragraph_format.space_before = Pt(6)
  203. p.paragraph_format.space_after = Pt(2)
  204. p.paragraph_format.keep_with_next = True
  205. drop_indent(p)
  206. r.add_picture(path, width=Cm(width))
  207. c, cr = self._new(caption)
  208. c.paragraph_format.line_spacing = 1.0
  209. c.alignment = WD_ALIGN_PARAGRAPH.CENTER
  210. c.paragraph_format.space_after = Pt(8)
  211. drop_indent(c)
  212. cr.font.size = Pt(CAP_PT)
  213. self.n_img += 1
  214. def done(self):
  215. self.a._p.getparent().remove(self.a._p)
  216. def find_placeholder(doc, needle, nth=0):
  217. hits = [p for p in doc.paragraphs if needle in p.text]
  218. return hits[nth]
  219. def main():
  220. dry = "--dry" in sys.argv
  221. if not os.path.isdir(PERF):
  222. raise SystemExit(f"缺少素材目录 {PERF}(先跑 _bid_prep_perf.py)")
  223. if not dry:
  224. bak = RESP[:-5] + f"_改价与业绩前备份_{time.strftime('%Y%m%d_%H%M')}.docx"
  225. shutil.copy2(RESP, bak)
  226. print(f"已备份 → {os.path.basename(bak)}\n")
  227. doc = Document(RESP)
  228. # ---- 1 报价
  229. n = 0
  230. for p in doc.paragraphs:
  231. for r in p.runs:
  232. if OLD_CN in r.text or OLD_NUM in r.text:
  233. r.text = r.text.replace(OLD_CN, NEW_CN).replace(OLD_NUM, NEW_NUM)
  234. n += 1
  235. for row in doc.tables[0].rows:
  236. for cell in row.cells:
  237. for p in cell.paragraphs:
  238. for r in p.runs:
  239. if OLD_CN in r.text or OLD_NUM in r.text:
  240. r.text = (r.text.replace(OLD_CN, NEW_CN)
  241. .replace(OLD_NUM, NEW_NUM))
  242. n += 1
  243. print(f"[1] 报价:改写 {n} 处 run → {NEW_CN} / ¥{NEW_NUM}")
  244. # ---- 2 业绩表(表2~表6 依次对应业绩1~5)
  245. for i, d in enumerate(PERFS):
  246. t = doc.tables[2 + i]
  247. vals = [d["name"], d["addr"], d["amount"], d["client"], d["date"], d["note"]]
  248. for row, v in zip(t.rows, vals):
  249. relax_row_height(row)
  250. put_cell(row.cells[1], v)
  251. print(f"[2] 业绩{d['no']} 表格已填:{d['name'][:30]}…")
  252. # ---- 3.1 合同关键页
  253. b = Block(find_placeholder(doc, "【待补:本项扫描件由供应商提供后插入】", 0))
  254. for d in PERFS:
  255. b.head(f"业绩{d['no']} {d['name']}")
  256. b.text(f"委托人:{d['client']} 合同签订日期:{d['date']} "
  257. f"合同金额:{d['amount']}")
  258. for tag, cap in CON_PAGES:
  259. b.img(os.path.join(PERF, f"{d['key']}_{tag}.jpg"),
  260. f"业绩{d['no']} {cap}")
  261. b.done()
  262. print(f"[3.1] 合同关键页:插入 {b.n_img} 页")
  263. # ---- 3.2 结算发票
  264. b = Block(find_placeholder(doc, "【待补:本项扫描件由供应商提供后插入】", 0))
  265. for d in PERFS:
  266. b.head(f"业绩{d['no']} 结算发票")
  267. b.text(d["note"])
  268. for fn, cap in d["inv"]:
  269. b.img(os.path.join(PERF, fn), cap, width=INV_W)
  270. b.done()
  271. print(f"[3.2] 结算发票:插入 {b.n_img} 张")
  272. # ---- 3.3 发票查验截图
  273. b = Block(find_placeholder(doc, "【待补:本项扫描件由供应商提供后插入】", 0))
  274. b.head("业绩3 增值税专用发票网上税务系统查验结果")
  275. b.text("以下为业绩3 对应 4 张增值税专用发票在国家税务总局全国增值税发票查验平台"
  276. "(https://inv-veri.chinatax.gov.cn/)的查验结果,"
  277. "发票代码 1100233130,开票日期均为 2024 年 2 月 6 日,"
  278. "价税合计合计 318,000.00 元。")
  279. for fn, cap in VERIFIED:
  280. b.img(os.path.join(PERF, fn), cap, width=INV_W)
  281. b.head("业绩1、2、4、5 电子发票查验结果")
  282. b.text("上述四张电子发票(增值税专用发票)的查验参数如下,查验结果截图待补:")
  283. for no, code, date, amt in PENDING_VERIFY:
  284. b.text(f"  业绩{no}:发票号码 {code},开票日期 {date},"
  285. f"价税合计 {amt} 元。")
  286. b.text("【待补:上述 4 张电子发票在 https://inv-veri.chinatax.gov.cn/ "
  287. "的查验结果截图】")
  288. b.done()
  289. print(f"[3.3] 查验截图:插入 {b.n_img} 张,另留 4 张待补")
  290. # ---- 劳动合同
  291. b = Block(find_placeholder(doc, "【待补:上表各岗位人员姓名及与我方签署的劳务合同"))
  292. b.head("项目团队人员劳动合同")
  293. b.text("我方与项目团队人员签订的劳动合同扫描件如下。")
  294. for start, name, post, term, cnt in LABOR:
  295. b.head(f"{name} {post} 合同期限 {term}")
  296. for k in range(cnt):
  297. b.img(os.path.join(PERF, f"lab_p{start + k}.jpg"),
  298. f"{name} 劳动合同第 {k + 1} 页")
  299. b.done()
  300. print(f"[4] 劳动合同:插入 {b.n_img} 页")
  301. if dry:
  302. print("\n--dry:未写回文档")
  303. return
  304. doc.save(RESP)
  305. mb = os.path.getsize(RESP) / 1024 / 1024
  306. print(f"\n已写回 {os.path.basename(RESP)} {mb:.1f}MB")
  307. if __name__ == "__main__":
  308. main()