_gen_bid_response.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. # -*- coding: utf-8 -*-
  2. """生成《响应文件》。
  3. 做法:直接抽取询比文件「第六章 响应文件格式」的原始 Word 内容(保留其全部原文、
  4. 字体、字号、缩进、对齐与表格样式),仅在原有空位上填写我方内容;不增删原文条款。
  5. 仅在原文明确允许「格式自拟」「供应商自行添加」处追加材料。
  6. 用法:python -X utf8 doc/_gen_bid_response.py
  7. """
  8. import copy
  9. import os
  10. import shutil
  11. import sys
  12. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  13. sys.stdout.reconfigure(encoding="utf-8", errors="replace")
  14. from docx import Document
  15. from docx.enum.text import WD_ALIGN_PARAGRAPH
  16. from docx.oxml import OxmlElement
  17. from docx.oxml.ns import qn
  18. from docx.shared import Cm
  19. from docx.table import Table
  20. from docx.text.paragraph import Paragraph
  21. from _bid_req_items import REQ_ITEMS
  22. from _gen_bid_annex import SW_COPYRIGHTS, SW_RELEVANCE
  23. DOC_DIR = os.path.dirname(os.path.abspath(__file__))
  24. SRC_TENDER = os.path.join(DOC_DIR, "_tmp_xunbi.docx")
  25. ONEDRIVE_DIR = r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标"
  26. OUT_NAME = "响应文件-2026年河南产互联制造业数据智能运营平台研发项目-北京智造易科技有限公司.docx"
  27. OUT_PATH = os.path.join(DOC_DIR, OUT_NAME)
  28. # ------------------------------------------------------------------ 填写内容
  29. PROJECT_FULL = "2026年河南产互联制造业数据智能运营平台研发项目"
  30. PROJECT_NO = "0635-202611FW0036"
  31. BUYER = "河南产业互联网联合发展有限公司"
  32. COMPANY = "北京智造易科技有限公司"
  33. USCC = "91110000MA00AFPA3H"
  34. LEGAL_REP = "刘涛"
  35. LEGAL_REP_SEX = "男"
  36. LEGAL_REP_AGE = "47" # 1978-12-11 生,按投标日 2026-08-05 计
  37. LEGAL_REP_ID = "340304197812110814"
  38. LEGAL_REP_TEL = "13955207130"
  39. AGENT = "郭峰"
  40. AGENT_ID = "320821198303277151"
  41. BANK_NAME = "中国建设银行股份有限公司北京天宝北街支行"
  42. BANK_ACCOUNT = "11050110248800000238"
  43. COMPANY_TYPE = "有限责任公司(自然人投资或控股)"
  44. # 注册地址以公共信用报告(2026-07-31 出具)载明的住所为准
  45. ADDRESS = "北京市北京经济技术开发区文化园西路6号院18号楼6层606"
  46. OPERATE_UNTIL = "2016年12月14日至2066年12月13日"
  47. REG_CAPITAL = "1000万元人民币"
  48. # 经营范围照录营业执照(登记机关:北京经济技术开发区市场监督管理局)
  49. BUSINESS_SCOPE = (
  50. "技术推广、技术咨询、技术服务、技术开发、技术转让;软件开发;网页设计;"
  51. "销售计算机软件及辅助设备、机械设备、电子产品、文化用品、通讯设备、"
  52. "针纺织品、五金产品(不含电动自行车);组织文化艺术交流活动;承办展览展示;"
  53. "会议服务;文艺创作;设计、制作、代理、发布广告;电脑图文设计、制作;"
  54. "摄影摄像服务;企业管理咨询;经济贸易咨询;企业策划;货物进出口;"
  55. "技术进出口;代理进出口;加工电子产品、计算机软件及辅助设备"
  56. "(限在外埠从事生产经营活动)。(市场主体依法自主选择经营项目,开展经营活动;"
  57. "依法须经批准的项目,经相关部门批准后依批准的内容开展经营活动;"
  58. "不得从事国家和本市产业政策禁止和限制类项目的经营活动。)")
  59. # 含税报价。另两家为 587,600 / 592,000,基准价=三家均价×97%;
  60. # 报价 560,000 时基准价 562,470.67,偏低 0.44%,得 39.87 分,领先次高 2.55 分。
  61. # 分差在临界价(563,651)及以下恒为 2.55,故取整数 56 万,避免报价贴住基准价。
  62. PRICE_CN = "伍拾陆万元整"
  63. PRICE_NUM = "560,000.00"
  64. BID_Y, BID_M, BID_D = "2026", "8", "5"
  65. PERF_COUNT = 5 # 业绩表份数
  66. TENDER_START_KEY = ("第六章", "响应文件格式")
  67. # ------------------------------------------------------------------ 工具函数
  68. def body_children(doc):
  69. return list(doc.element.body.iterchildren())
  70. def para_text(el):
  71. return "".join(n.text or "" for n in el.iter(qn("w:t")))
  72. def trim_to_chapter6(doc):
  73. """删除第六章之外的全部 body 元素,仅保留第六章(不含第七章)。"""
  74. children = body_children(doc)
  75. c6 = [i for i, el in enumerate(children)
  76. if el.tag == qn("w:p")
  77. and para_text(el).strip().startswith(TENDER_START_KEY[0])
  78. and TENDER_START_KEY[1] in para_text(el)]
  79. c7 = [i for i, el in enumerate(children)
  80. if el.tag == qn("w:p") and para_text(el).strip().startswith("第七章")]
  81. start = c6[-1]
  82. end = next(i for i in c7 if i > start)
  83. body = doc.element.body
  84. sect = body.find(qn("w:sectPr"))
  85. for i, el in enumerate(children):
  86. if el.tag == qn("w:sectPr"):
  87. continue
  88. if i < start or i >= end:
  89. body.remove(el)
  90. if sect is not None and sect.getparent() is None:
  91. body.append(sect)
  92. return start
  93. def set_run(p, idx, text):
  94. """替换段落第 idx 个 run 的文本,完整保留该 run 的字体格式。"""
  95. runs = p.runs
  96. if idx >= len(runs):
  97. raise IndexError(f"run {idx} 不存在(共 {len(runs)} 个):{p.text!r}")
  98. runs[idx].text = text
  99. def append_run_like(p, src_idx, text):
  100. """按第 src_idx 个 run 的格式,在段落末尾追加一个新 run。"""
  101. src = p.runs[src_idx]
  102. new = copy.deepcopy(src._element)
  103. p._p.append(new)
  104. from docx.text.run import Run
  105. r = Run(new, p)
  106. r.text = text
  107. return r
  108. def cell_fill(cell, text, like_cell=None):
  109. """填写表格单元格,套用同表已有单元格的字体格式;\n 转为段内换行。
  110. 会清空该单元格原有全部段落内容(保留第一个段落及其段落格式)。
  111. """
  112. tmpl_run = None
  113. if like_cell is not None:
  114. for pp in like_cell.paragraphs:
  115. if pp.runs:
  116. tmpl_run = pp.runs[0]._element
  117. break
  118. paras = cell.paragraphs
  119. for extra in paras[1:]:
  120. extra._p.getparent().remove(extra._p)
  121. p = cell.paragraphs[0]
  122. for r in list(p.runs):
  123. r._element.getparent().remove(r._element)
  124. from docx.text.run import Run
  125. def new_run():
  126. if tmpl_run is None:
  127. return p.add_run()
  128. el = copy.deepcopy(tmpl_run)
  129. for child in list(el):
  130. if child.tag in (qn("w:t"), qn("w:br"), qn("w:cr"), qn("w:tab")):
  131. el.remove(child)
  132. p._p.append(el)
  133. return Run(el, p)
  134. for i, line in enumerate(str(text).split("\n")):
  135. if i > 0:
  136. new_run()._element.append(OxmlElement("w:br"))
  137. new_run().text = line
  138. def _page_field_para():
  139. p = OxmlElement("w:p")
  140. pPr = OxmlElement("w:pPr")
  141. st = OxmlElement("w:pStyle")
  142. st.set(qn("w:val"), "21")
  143. jc = OxmlElement("w:jc")
  144. jc.set(qn("w:val"), "center")
  145. pPr.append(st)
  146. pPr.append(jc)
  147. p.append(pPr)
  148. for kind, payload in (("begin", None), ("instr", " PAGE \\* MERGEFORMAT "),
  149. ("separate", None), ("text", "1"), ("end", None)):
  150. r = OxmlElement("w:r")
  151. if kind == "instr":
  152. it = OxmlElement("w:instrText")
  153. it.set(qn("xml:space"), "preserve")
  154. it.text = payload
  155. r.append(it)
  156. elif kind == "text":
  157. t = OxmlElement("w:t")
  158. t.text = payload
  159. r.append(t)
  160. else:
  161. fc = OxmlElement("w:fldChar")
  162. fc.set(qn("w:fldCharType"), kind)
  163. r.append(fc)
  164. p.append(r)
  165. return p
  166. def fix_footer_page_numbers(doc):
  167. """模板页脚把页码放在 wrap=none 自动缩放的文本框里,两位数会被压掉。
  168. 改为每一节都显式挂一个标准的居中 PAGE 域(视觉仍是底部居中页码),
  169. 既修掉两位数被裁的问题,也避免删除首页后各节页脚失联。
  170. """
  171. n = 0
  172. for sec in doc.sections:
  173. sec.different_first_page_header_footer = False
  174. sec.footer.is_linked_to_previous = False
  175. ftr = sec.footer._element
  176. for child in list(ftr):
  177. ftr.remove(child)
  178. ftr.append(_page_field_para())
  179. n += 1
  180. return n
  181. def relax_row_height(row):
  182. """模板行高为「固定值」,长原文会被裁切;改为「最小值」让该行自动增高。
  183. 仅动 hRule,保留原高度作为下限,字体、边框、列宽均不变。
  184. """
  185. trPr = row._tr.get_or_add_trPr()
  186. th = trPr.find(qn("w:trHeight"))
  187. if th is not None:
  188. th.set(qn("w:hRule"), "atLeast")
  189. def clone_para_after(anchor_el, like_el, doc, text=None):
  190. """在 anchor_el 之后插入一个新段落,段落与首个 run 的格式均取自 like_el。"""
  191. el = copy.deepcopy(like_el)
  192. anchor_el.addnext(el)
  193. p = Paragraph(el, doc)
  194. for extra in list(p.runs)[1:]:
  195. extra._element.getparent().remove(extra._element)
  196. # 模板段落可能带自动编号或分页属性,克隆后需清掉,避免附件标题被编号/乱分页
  197. pPr = el.find(qn("w:pPr"))
  198. if pPr is not None:
  199. numPr = pPr.find(qn("w:numPr"))
  200. if numPr is not None:
  201. pPr.remove(numPr)
  202. p.paragraph_format.page_break_before = False
  203. if p.runs:
  204. p.runs[0].text = "" if text is None else text
  205. else:
  206. p.add_run(text or "")
  207. return p
  208. ATTACH_DIR = os.path.join(DOC_DIR, "_bid_assets", "attach")
  209. CONTENT_W, CONTENT_H = 14.6, 24.6 # 模板正文区尺寸(cm)
  210. MERGE_TOKEN = "@@MERGE:%s@@" # 供合并脚本定位并替换为外部文件内容
  211. def outline(p, lvl):
  212. """只写 w:outlineLvl,不套 Heading 样式。
  213. 导航窗格与目录域取的是「大纲级别」而非样式名,因此这样即可得到完整层级
  214. 与自动目录,而询比文件原有的字体、字号、缩进、对齐全部保持不动。
  215. """
  216. pPr = p._p.get_or_add_pPr()
  217. old = pPr.find(qn("w:outlineLvl"))
  218. if old is not None:
  219. pPr.remove(old)
  220. el = OxmlElement("w:outlineLvl")
  221. el.set(qn("w:val"), str(lvl - 1))
  222. pPr.append(el)
  223. return p
  224. def fit(path, max_w, max_h):
  225. """按正文区等比缩放,返回 (宽 cm, 高 cm)。"""
  226. from PIL import Image as _Image
  227. w_px, h_px = _Image.open(path).size
  228. w, h = max_w, max_w * h_px / w_px
  229. if h > max_h:
  230. h, w = max_h, max_h * w_px / h_px
  231. return w, h
  232. def sub(cursor_el, text, like_el, doc, lvl=None, page_break=False):
  233. """插入一个从属段落(组织标题、说明或占位提示),返回段落对象。"""
  234. p = clone_para_after(cursor_el, like_el, doc, text)
  235. p.alignment = WD_ALIGN_PARAGRAPH.LEFT
  236. if page_break:
  237. p.paragraph_format.page_break_before = True
  238. if lvl:
  239. outline(p, lvl)
  240. return p
  241. def merge_mark(cursor_el, key, like_el, doc):
  242. """插入合并占位标记;合并脚本会整段删除它并在原位插入对应文件。
  243. 不加段前分页:分页由合并时插入的分节符或前面的组织标题负责,
  244. 否则会多出一页空白。
  245. """
  246. return clone_para_after(cursor_el, like_el, doc, MERGE_TOKEN % key)._p
  247. def attachment(cursor_el, title, pages, like_el, doc, max_w=CONTENT_W,
  248. note=None, lvl=None, page_break=True):
  249. """插入一个附件块:标题另起页,随后按 pages 逐页排图。
  250. pages 为「每页一组图」的列表,元素形如 [(文件名, 图注 or None), ...];
  251. 同一组内的图排在同一页。返回 (标题段, 最后一个元素),便于连续插入。
  252. """
  253. cur = clone_para_after(cursor_el, like_el, doc, title)
  254. cur.paragraph_format.page_break_before = page_break
  255. cur.alignment = WD_ALIGN_PARAGRAPH.LEFT
  256. if lvl:
  257. outline(cur, lvl)
  258. last = cur._p
  259. for line in ([note] if isinstance(note, str) else (note or [])):
  260. last = clone_para_after(last, like_el, doc, line)._p
  261. for gi, group in enumerate(pages):
  262. # 每页高度按该页图片数量均分,避免同页多图溢出
  263. share = (CONTENT_H - 3.0) / max(1, len(group))
  264. for fn, cap in group:
  265. path = os.path.join(ATTACH_DIR, fn)
  266. if not os.path.exists(path):
  267. raise FileNotFoundError(path)
  268. w, _ = fit(path, max_w, share)
  269. ip = clone_para_after(last, like_el, doc)
  270. ip.alignment = WD_ALIGN_PARAGRAPH.CENTER
  271. if gi > 0 and group[0][0] == fn:
  272. ip.paragraph_format.page_break_before = True
  273. ip.runs[0].add_picture(path, width=Cm(w))
  274. last = ip._p
  275. if cap:
  276. cp = clone_para_after(last, like_el, doc, cap)
  277. cp.alignment = WD_ALIGN_PARAGRAPH.CENTER
  278. last = cp._p
  279. return cur, last
  280. def add_bookmark(p, name, bid):
  281. start = OxmlElement("w:bookmarkStart")
  282. start.set(qn("w:id"), str(bid))
  283. start.set(qn("w:name"), name)
  284. end = OxmlElement("w:bookmarkEnd")
  285. end.set(qn("w:id"), str(bid))
  286. p._p.insert(0, start)
  287. p._p.append(end)
  288. # ------------------------------------------------------------------ 主流程
  289. shutil.copy2(SRC_TENDER, OUT_PATH + ".tmp")
  290. doc = Document(OUT_PATH + ".tmp")
  291. OFF = trim_to_chapter6(doc)
  292. ch = body_children(doc)
  293. def P(old_idx):
  294. return Paragraph(ch[old_idx - OFF], doc)
  295. def T(old_idx):
  296. return Table(ch[old_idx - OFF], doc)
  297. SEALS = [] # (bookmark, 章节, 盖章要求, 签字要求, 依据)
  298. def seal(p, key, section, stamp, sign, basis):
  299. add_bookmark(p, key, 1000 + len(SEALS))
  300. SEALS.append((key, section, stamp, sign, basis))
  301. # ---------------- 章节标题:补原文漏掉的编号,并设大纲级别
  302. # 「资格审查资料:」看似缺“四、”,实为该段带自动编号(numId 52)已渲染出序号,
  303. # 不能再补文字,否则目录会显示成“四、四、资格审查资料”。
  304. # 「包括但不限于以下资料…」确无编号,补“(三)”后目录编号才连贯。
  305. p = P(935)
  306. set_run(p, 0, "(三)包括但不限于以下资料(")
  307. # 原文把「技术要求的汇总应答」也编成“(一)”,与上一节重号,按序改为“(二)”
  308. set_run(P(924), 0, "(二)技术要求的汇总应答")
  309. for _i in (792, 816, 838, 853, 889, 915, 942): # 一~七
  310. outline(P(_i), 1)
  311. for _i in (794, 806, 854, 858, 890, 898, 917, 924, 935):
  312. outline(P(_i), 2)
  313. # ---------------- 目录:把原文手工目录清单换成自动目录域
  314. # 原文第 768 段本就有「目 录」标题页,其下 770~777 是七条无页码的手工条目。
  315. # 换成 TOC 域后可显示到四级并带页码与前导点,标题页与页面格式保持原样。
  316. def toc_field(p, instr=' TOC \\o "1-4" \\h \\z \\u '):
  317. for r in list(p.runs):
  318. r._element.getparent().remove(r._element)
  319. for kind, payload in (("begin", None), ("instr", instr),
  320. ("separate", None),
  321. ("text", "(打开文档后请右键选择“更新域”刷新页码)"),
  322. ("end", None)):
  323. r = OxmlElement("w:r")
  324. if kind == "instr":
  325. it = OxmlElement("w:instrText")
  326. it.set(qn("xml:space"), "preserve")
  327. it.text = payload
  328. r.append(it)
  329. elif kind == "text":
  330. t = OxmlElement("w:t")
  331. t.text = payload
  332. r.append(t)
  333. else:
  334. fc = OxmlElement("w:fldChar")
  335. fc.set(qn("w:fldCharType"), kind)
  336. r.append(fc)
  337. p._p.append(r)
  338. toc_field(P(770))
  339. for _i in range(771, 792): # 余下手工条目与撑页空段一并去掉
  340. _el = ch[_i - OFF]
  341. if _el.getparent() is not None:
  342. _el.getparent().remove(_el)
  343. P(792).paragraph_format.page_break_before = True # 正文另起页
  344. # 末尾「七、其他」之后的空段会多撑出一整页,去掉
  345. _body = doc.element.body
  346. while True:
  347. _ps = _body.findall(qn("w:p"))
  348. if not _ps:
  349. break
  350. _txt = "".join(n.text or "" for n in _ps[-1].iter(qn("w:t")))
  351. if _txt.strip():
  352. break
  353. _body.remove(_ps[-1])
  354. # ---------------- 封面
  355. p = P(749)
  356. set_run(p, 0, " 2026年河南产互联制造业数据智能运营平台研发")
  357. p = P(756)
  358. set_run(p, 0, f"项目编号:{PROJECT_NO}")
  359. p = P(764)
  360. set_run(p, 1, f" {COMPANY} (盖公章)")
  361. seal(p, "SEAL01", "封面", "供应商(盖公章)",
  362. "法定代表人或其授权代表(签字或盖章)",
  363. "第六章「响应文件格式」封面署名栏原文")
  364. p = P(766)
  365. set_run(p, 0, f"日 期: {BID_Y} 年 {BID_M} 月 {BID_D} 日")
  366. # ---------------- 一(一)响应函
  367. p = P(795)
  368. set_run(p, 1, BUYER)
  369. p = P(796)
  370. set_run(p, 1, f" {PROJECT_NO} ")
  371. set_run(p, 3, f" {PROJECT_FULL}")
  372. set_run(p, 6, f" {PRICE_CN} ")
  373. set_run(p, 8, f" {PRICE_NUM} ")
  374. p = P(799)
  375. set_run(p, 1, " 1年 ")
  376. p = P(803)
  377. set_run(p, 1, f" {COMPANY} (盖章)")
  378. seal(p, "SEAL02", "一(一)响应函", "供应商(盖章)",
  379. "法定代表人或其委托代理人(签字或盖章)",
  380. "第六章响应函署名栏原文;并为第三章形式评审项「响应函签字盖章」")
  381. p = P(805)
  382. set_run(p, 1, f" {BID_Y} ")
  383. set_run(p, 3, f" {BID_M} ")
  384. set_run(p, 5, f" {BID_D} ")
  385. # ---------------- 一(二)响应函附录
  386. t = T(807)
  387. lab = t.cell(0, 0)
  388. cell_fill(t.cell(0, 1), PROJECT_FULL, lab)
  389. cell_fill(t.cell(1, 1), PROJECT_NO, lab)
  390. cell_fill(t.cell(2, 1), COMPANY, lab)
  391. cell_fill(t.cell(3, 1), f"大写:{PRICE_CN};\n小写:¥{PRICE_NUM} 元", lab)
  392. cell_fill(t.cell(4, 1),
  393. "2026年河南产互联制造业数据智能运营平台研发,包括故障处理、系统巡检、"
  394. "版本升级、技术咨询等", lab)
  395. cell_fill(t.cell(5, 1), "1年", lab)
  396. cell_fill(t.cell(6, 1), "系统提供不低于1年的免费质保期。", lab)
  397. cell_fill(t.cell(7, 1), "符合国家、省、市相关规定及行业要求,满足采购人工作规范要求。", lab)
  398. cell_fill(t.cell(8, 1), "采购人指定地点", lab)
  399. p = P(811)
  400. set_run(p, 1, f" {COMPANY} (盖章)")
  401. seal(p, "SEAL03", "一(二)响应函附录", "供应商(盖章)",
  402. "法定代表人或其委托代理人(签字或盖章)", "第六章响应函附录署名栏原文")
  403. p = P(813)
  404. set_run(p, 1, f" {BID_Y} ")
  405. set_run(p, 3, f" {BID_M} ")
  406. set_run(p, 5, f" {BID_D} ")
  407. # ---------------- 二 法定代表人身份证明书
  408. set_run(P(818), 2, f" {COMPANY} ")
  409. set_run(P(819), 1, f" {COMPANY_TYPE} ")
  410. set_run(P(820), 1, f" {ADDRESS} ")
  411. p = P(821)
  412. set_run(p, 1, " 2016 ")
  413. set_run(p, 3, " 12 ")
  414. set_run(p, 5, " 14 ")
  415. set_run(P(822), 1, f" {OPERATE_UNTIL} ")
  416. p = P(823)
  417. set_run(p, 1, f" {LEGAL_REP} ")
  418. set_run(p, 3, f" {LEGAL_REP_SEX} ")
  419. set_run(p, 5, f" {LEGAL_REP_AGE} ")
  420. set_run(p, 7, "【待填:职务】")
  421. set_run(P(824), 1, f" {COMPANY} ")
  422. p = P(831)
  423. set_run(p, 1, f" {COMPANY} (盖章)")
  424. seal(p, "SEAL04", "二、法定代表人身份证明书", "供应商(盖章)",
  425. "法定代表人(签字或盖章)",
  426. "第六章署名栏原文写「法定代表人」;另原文明文「附:法定代表人身份证复印件,加盖单位公章」")
  427. p = P(833)
  428. set_run(p, 1, f" {BID_Y} ")
  429. set_run(p, 3, f" {BID_M} ")
  430. set_run(p, 5, f" {BID_D} ")
  431. # 法定代表人身份证复印件(原文「附:法定代表人身份证复印件,加盖单位公章」)
  432. tp, _ = attachment(ch[836 - OFF], "附:法定代表人身份证复印件(正面、反面)",
  433. [[("id_front.jpg", "(正面)"), ("id_back.jpg", "(反面)")]],
  434. ch[836 - OFF], doc, max_w=12.4)
  435. seal(tp, "SEALA1", "二、法定代表人身份证复印件", "加盖单位公章", "—",
  436. "第六章「附:法定代表人身份证复印件,加盖单位公章。」")
  437. # ---------------- 三 法定代表人授权委托书
  438. p = P(840)
  439. set_run(p, 1, f" {LEGAL_REP} ")
  440. set_run(p, 3, f" {COMPANY} ")
  441. set_run(p, 5, f" {AGENT} ")
  442. set_run(p, 7, f" {COMPANY} ")
  443. set_run(p, 9, f" {BUYER} ")
  444. set_run(p, 11, f" {PROJECT_FULL} ")
  445. p = P(843)
  446. set_run(p, 1, f" {COMPANY} ")
  447. seal(p, "SEAL05", "三、法定代表人授权委托书", "供应商(盖单位章)",
  448. "法定代表人(签字或盖章)",
  449. "第六章署名栏原文写「法定代表人」;另原文明文「附:委托代理人身份证复印件,加盖单位公章」")
  450. set_run(P(845), 1, f" {LEGAL_REP_ID} ")
  451. set_run(P(846), 1, f" {AGENT} ")
  452. set_run(P(847), 1, f" {AGENT_ID} ")
  453. p = P(848) # 该段空格 run 为 0/2/4,年/月/日 为 1/3/5
  454. set_run(p, 0, f" {BID_Y} ")
  455. set_run(p, 2, f" {BID_M} ")
  456. set_run(p, 4, f" {BID_D} ")
  457. # ---------------- 四(一)供应商基本情况表
  458. t = T(855)
  459. lab = t.cell(0, 0)
  460. cell_fill(t.cell(0, 1), COMPANY, lab)
  461. cell_fill(t.cell(1, 1), ADDRESS, lab)
  462. cell_fill(t.cell(1, 4), "2016年12月14日", lab)
  463. cell_fill(t.cell(2, 2), LEGAL_REP, lab)
  464. cell_fill(t.cell(2, 4), LEGAL_REP_TEL, lab)
  465. cell_fill(t.cell(3, 2), "【待填:项目负责人姓名】", lab)
  466. cell_fill(t.cell(3, 4), "【待填:项目负责人电话】", lab)
  467. cell_fill(t.cell(4, 1), REG_CAPITAL, lab)
  468. cell_fill(t.cell(5, 1), USCC, lab)
  469. cell_fill(t.cell(6, 1), BANK_NAME, lab)
  470. cell_fill(t.cell(7, 1), BANK_ACCOUNT, lab)
  471. cell_fill(t.cell(8, 1), "【待填:办公场所面积】", lab)
  472. cell_fill(t.cell(9, 1), BUSINESS_SCOPE, lab)
  473. relax_row_height(t.rows[9]) # 经营范围原文较长,固定行高会裁字
  474. cell_fill(t.cell(10, 0), "员工总人数(【待填】人)", lab)
  475. # ---------------- 四(二)供应商资格声明函
  476. set_run(P(860), 1, f" {BUYER} ")
  477. p = P(874)
  478. set_run(p, 1, f" {COMPANY} (盖章)")
  479. seal(p, "SEAL06", "四(二)供应商资格声明函", "供应商(盖章)",
  480. "法定代表人或其委托代理人(签字或盖章)", "第六章供应商资格声明函署名栏原文")
  481. p = P(876)
  482. set_run(p, 1, f" {BID_Y} ")
  483. set_run(p, 3, f" {BID_M} ")
  484. set_run(p, 5, f" {BID_D} ")
  485. # ---------------- 四(三)证明资料:逐条对应资格条件 3.1~3.6
  486. like = ch[884 - OFF] # 借用同节段落与字体格式
  487. last = sub(ch[887 - OFF], "(三)证明资料", like, doc, lvl=2,
  488. page_break=True)._p
  489. # 1、营业执照(资格条件 3.1):紧跟「(三)证明资料」同页,不另起页
  490. _, last = attachment(last, "1、营业执照", [[("license.jpg", None)]],
  491. like, doc, lvl=3, page_break=False)
  492. # 2、增值税专用发票承诺书(资格条件 3.2,明文「提供承诺书」)
  493. last = sub(last, "2、能够提供增值税专用发票的承诺书", like, doc, lvl=3,
  494. page_break=True)._p
  495. last = merge_mark(last, "invoice", like, doc)
  496. # 3、类似业绩(资格条件 3.3,同时对应「类似业绩」10 分)
  497. last = sub(last, "3、2023年1月1日以来签订的软件开发项目案例", like, doc,
  498. lvl=3, page_break=True)._p
  499. last = sub(last, "注:业绩汇总表见本响应文件「五、商务响应文件」5.1 企业业绩,"
  500. "本节按每个业绩依次附合同关键页、结算发票及发票查验截图。",
  501. like, doc)._p
  502. for tag, name in (("3.1", "合同(首页、服务内容页、金额页、签字盖章页)"),
  503. ("3.2", "发票(结算发票扫描件)"),
  504. ("3.3", "发票查询(网上税务系统查验截图)")):
  505. last = sub(last, f"{tag} {name}", like, doc, lvl=4)._p
  506. last = sub(last, "【待补:本项扫描件由供应商提供后插入】", like, doc)._p
  507. # 4、不组成联合体及中选后不转包承诺书(资格条件 3.4)
  508. last = sub(last, "4、我公司在本项目不是联合体应答、中选后不转包的承诺书",
  509. like, doc, lvl=3, page_break=True)._p
  510. last = merge_mark(last, "consortium", like, doc)
  511. # 5、信用记录(资格条件 3.5,明文「提供相关网页查询截图」)
  512. last = sub(last, "5、未被列入各个网站违法失信名单的承诺书", like, doc,
  513. lvl=3, page_break=True)._p
  514. CREDIT_SITES = [
  515. ("5.1", "未被“国家企业信用信息公示系统”列入“经营异常名录”"),
  516. ("5.2", "未被“国家企业信用信息公示系统”列入“严重违法失信名单”"),
  517. ("5.3", "未被“信用中国”网站列入“失信被执行人”"),
  518. ("5.4", "未被“信用中国”网站列入“重大税收违法失信主体”"),
  519. ("5.5", "未被“信用中国”网站列入“政府采购严重违法失信行为记录名单”"),
  520. ]
  521. for tag, name in CREDIT_SITES:
  522. last = sub(last, f"{tag} {name}", like, doc, lvl=4)._p
  523. last = sub(last, "【待补:网页查询截图由供应商提供后插入】", like, doc)._p
  524. _, last = attachment(last, "5.6 公共信用信息报告",
  525. [[(f"credit_{i:02d}.jpg", None)] for i in range(1, 8)],
  526. like, doc, lvl=4,
  527. note="说明:本报告由国家公共信用和地理空间信息中心于 2026 年 7 月 31 日出具"
  528. "(报告编号 202607311929041364K204),载明我方严重失信 0 条、"
  529. "经营异常 0 条、司法判决 0 条。")
  530. # 6、不存在下列情形之一的承诺书(资格条件 3.6,明文「提供承诺书」)
  531. last = sub(last, "6、我公司不存在下列情形之一的承诺书", like, doc, lvl=3,
  532. page_break=True)._p
  533. last = merge_mark(last, "credit", like, doc)
  534. # ---------------- 五 5.1 企业业绩(一个业绩一张表,供应商自行添加)
  535. perf_tbl = ch[891 - OFF]
  536. perf_blk = [ch[i - OFF] for i in (892, 893, 894, 895)]
  537. anchor = perf_blk[-1]
  538. for n in range(PERF_COUNT, 0, -1):
  539. if n == 1:
  540. tbl_el, blk_els = perf_tbl, perf_blk
  541. else:
  542. tbl_el = copy.deepcopy(perf_tbl)
  543. blk_els = [copy.deepcopy(e) for e in perf_blk]
  544. cur = anchor
  545. for e in [tbl_el] + blk_els:
  546. cur.addnext(e)
  547. cur = e
  548. tb = Table(tbl_el, doc)
  549. lb = tb.cell(0, 0)
  550. cell_fill(tb.cell(0, 1), f"【待填:业绩{n}·项目名称】", lb)
  551. cell_fill(tb.cell(1, 1), f"【待填:业绩{n}·项目地址】", lb)
  552. cell_fill(tb.cell(2, 1), f"【待填:业绩{n}·项目金额(不低于20万元)】", lb)
  553. cell_fill(tb.cell(3, 1), f"【待填:业绩{n}·委托人名称】", lb)
  554. cell_fill(tb.cell(4, 1), f"【待填:业绩{n}·合同签订日期(2023年1月1日以来)】", lb)
  555. cell_fill(tb.cell(5, 1), f"【待填:业绩{n}·结算发票号码及金额】", lb)
  556. pp = Paragraph(blk_els[1], doc)
  557. set_run(pp, 1, f" {COMPANY} ")
  558. seal(pp, f"SEALP{n}", f"五、5.1 企业业绩 · 业绩表 {n}", "供应商名称(盖章)",
  559. "法定代表人或其授权代表(签字或盖章)",
  560. "第六章「5.1企业业绩(一个业绩一张表,供应商自行添加)」署名栏原文")
  561. pd = Paragraph(blk_els[3], doc)
  562. set_run(pd, 1, f" {BID_Y} ")
  563. set_run(pd, 3, f" {BID_M} ")
  564. set_run(pd, 5, f" {BID_D} ")
  565. # ---------------- 五 5.2 采购活动廉洁承诺书
  566. set_run(P(901), 0, f" {BUYER} ")
  567. set_run(P(902), 1, f" {PROJECT_FULL} ")
  568. set_run(P(910), 1, f" {COMPANY} ")
  569. p = P(912)
  570. append_run_like(p, 0, f" {COMPANY} ")
  571. seal(p, "SEAL07", "五、5.2 采购活动廉洁承诺书", "承诺单位(签章)",
  572. "法定代表人或授权代理人签字或盖章", "第六章采购活动廉洁承诺书署名栏原文")
  573. set_run(P(913), 0, f"{BID_Y} 年 {BID_M} 月 {BID_D} 日")
  574. # ---------------- 六 技术响应文件:两处「全部满足」
  575. set_run(P(919), 3, " 全部满足 ")
  576. set_run(P(926), 3, " 全部满足 ")
  577. # 采购需求逐条应答表:在原表内追加行(原表 1 表头 + 2 空行)
  578. t = T(928)
  579. hdr = t.rows[0]
  580. tmpl_row = t.rows[1]._tr
  581. for r in list(t.rows[1:]):
  582. r._tr.getparent().remove(r._tr)
  583. for no, txt in REQ_ITEMS:
  584. new_tr = copy.deepcopy(tmpl_row)
  585. t._tbl.append(new_tr)
  586. from docx.table import _Row
  587. row = _Row(new_tr, t)
  588. cell_fill(row.cells[0], no, hdr.cells[0])
  589. cell_fill(row.cells[1], txt, hdr.cells[0])
  590. cell_fill(row.cells[2], "完全响应", hdr.cells[0])
  591. cell_fill(row.cells[3], "", hdr.cells[0])
  592. p = P(931)
  593. set_run(p, 2, f" {COMPANY} ")
  594. seal(p, "SEAL08", "六、技术响应文件(汇总应答之后)", "供应商名称(盖单位章)",
  595. "法定代表人或其委托代理人(签字)",
  596. "第六章技术响应文件署名栏原文(原文为「签字」);全章仅此一处署名栏,"
  597. "同时覆盖合同条款汇总应答与采购需求汇总应答")
  598. p = P(933)
  599. set_run(p, 0, f" {BID_Y} ")
  600. set_run(p, 2, f" {BID_M} ")
  601. set_run(p, 4, f" {BID_D} ")
  602. # ---------------- 六(三)格式自拟材料:企业实力、软著、项目团队、技术方案各章
  603. like = ch[936 - OFF]
  604. # 1、软件企业证书及软件产品证书(企业实力 6 分)
  605. _, last = attachment(
  606. ch[938 - OFF], "1、软件企业证书及软件产品证书",
  607. [[("sw_enterprise.jpg", "软件企业证书(证书编号 京 RQ-2025-1622)")],
  608. [("sw_product.jpg", "软件产品证书(证书编号 京 RC-2025-1407)")]],
  609. like, doc, lvl=3,
  610. note="说明:我方持有软件企业证书(北京软件和信息服务业协会,证书编号 京RQ-2025-1622,"
  611. "2025年11月29日发证)与软件产品证书(中国软件行业协会,证书编号 京RC-2025-1407,"
  612. "评估软件为透明工厂管理平台V1.0),两证均在有效期内,"
  613. "证书主体均为北京智造易科技有限公司。")
  614. # 2、软件著作权登记证书(软著 9 分):先一览表,再逐份证书扫描件
  615. last = sub(last, "2、计算机软件著作权登记证书", like, doc, lvl=3,
  616. page_break=True)._p
  617. last = merge_mark(last, "swtable", like, doc)
  618. _, last = attachment(
  619. last, "证书扫描件(共 10 份)",
  620. [[(f"swc_{i:02d}.jpg", f"{SW_COPYRIGHTS[i - 1][0]}({SW_COPYRIGHTS[i - 1][1]})")]
  621. for i in range(1, 11)],
  622. like, doc)
  623. # 3、项目团队(项目团队 2 分)
  624. last = sub(last, "3、项目团队", like, doc, lvl=3, page_break=True)._p
  625. last = merge_mark(last, "team", like, doc)
  626. last = sub(last, "【待补:上表各岗位人员姓名及与我方签署的劳务合同扫描件,"
  627. "由供应商提供后插入】", like, doc)._p
  628. # 4~12:技术方案与实施服务方案各章,章标题自带序号与大纲级别
  629. for _i in range(4, 13):
  630. last = merge_mark(last, f"ch{_i:02d}", like, doc)
  631. # 「七、其他」原为空章节:另起页并作出应答,避免标题挤在上一章末尾、末页全白。
  632. # 该段末尾带模板自留的 <w:br type="page">(原用于收尾第六章),不去掉会把
  633. # 下面的应答顶到新的一页。
  634. _other = P(942)
  635. _other.paragraph_format.page_break_before = True
  636. for _r in list(_other.runs):
  637. if _r._element.findall(qn("w:br")) and not _r.text:
  638. _r._element.getparent().remove(_r._element)
  639. sub(ch[942 - OFF], "除本响应文件已列明的资格审查资料、商务响应文件与技术响应文件外,"
  640. "我方无其他需要补充说明的事项。", like, doc)
  641. print("[结构] 已按截图重建四(三)证明资料 1~6、六(三)1~12 层级")
  642. # 去掉询比文件的「第六章 响应文件格式」标题页,响应文件从封面开始
  643. title_el = ch[747 - OFF]
  644. title_el.getparent().remove(title_el)
  645. print("[标题页] 已移除询比文件「第六章 响应文件格式」标题页")
  646. n_ftr = fix_footer_page_numbers(doc)
  647. print(f"[页脚] 已修正 {n_ftr} 个页脚的页码域(模板文本框会压掉两位数)")
  648. doc.save(OUT_PATH)
  649. os.remove(OUT_PATH + ".tmp")
  650. print(f"[完成] {OUT_PATH} ({os.path.getsize(OUT_PATH):,} bytes)")
  651. print(f"[签章位] 共 {len(SEALS)} 处(均为询比文件明文要求)")
  652. import json
  653. with open(os.path.join(DOC_DIR, "_tmp_seals.json"), "w", encoding="utf-8") as f:
  654. json.dump(SEALS, f, ensure_ascii=False, indent=2)
  655. if os.path.isdir(ONEDRIVE_DIR):
  656. try:
  657. shutil.copy2(OUT_PATH, os.path.join(ONEDRIVE_DIR, OUT_NAME))
  658. print(f"[已同步] {os.path.join(ONEDRIVE_DIR, OUT_NAME)}")
  659. except Exception as e:
  660. print(f"[同步失败] {type(e).__name__}: {e}")