_gen_bid_action_excel.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. # -*- coding: utf-8 -*-
  2. """Generate AIDOP bidding preparation action Excel."""
  3. from decimal import Decimal, ROUND_HALF_UP
  4. from openpyxl import Workbook
  5. from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
  6. from openpyxl.utils import get_column_letter
  7. from openpyxl.worksheet.datavalidation import DataValidation
  8. CAP = Decimal("600000")
  9. TAX_RATE = Decimal("0.06")
  10. CENT = Decimal("0.01")
  11. def money(x):
  12. return Decimal(x).quantize(CENT, rounding=ROUND_HALF_UP)
  13. def fmt(x):
  14. return f"{money(x):,.2f}"
  15. def baseline(prices):
  16. return money(sum(Decimal(p) for p in prices) / Decimal(3) * Decimal("0.97"))
  17. def bid_score(price, base):
  18. """40 at baseline; -0.6/1% above, -0.3/1% below; 2dp, floor 0."""
  19. price = Decimal(price)
  20. if price == base:
  21. return Decimal("40.00")
  22. pct = (price - base) / base * Decimal("100")
  23. penalty = pct * Decimal("0.6") if price > base else -pct * Decimal("0.3")
  24. return max((Decimal("40") - penalty).quantize(CENT, rounding=ROUND_HALF_UP), Decimal("0"))
  25. def solve_our_price(p2, p3):
  26. """Largest price equal to the resulting baseline: p1 = 0.97*(p1+p2+p3)/3."""
  27. raw = money(Decimal("0.97") * (Decimal(p2) + Decimal(p3)) / Decimal("2.03"))
  28. for adj in range(20, -21, -1):
  29. cand = raw + Decimal(adj) / 100
  30. if cand == baseline([cand, p2, p3]):
  31. return cand
  32. return raw
  33. def ex_tax(price):
  34. return money(Decimal(price) / (Decimal("1") + TAX_RATE))
  35. class Plan:
  36. def __init__(self, key, name, p2, p3, verdict, comment, our_price=None):
  37. self.key = key
  38. self.name = name
  39. self.p2 = Decimal(p2)
  40. self.p3 = Decimal(p3)
  41. self.p1 = Decimal(our_price) if our_price is not None else solve_our_price(p2, p3)
  42. self.base = baseline([self.p1, self.p2, self.p3])
  43. self.s1 = bid_score(self.p1, self.base)
  44. self.s2 = bid_score(self.p2, self.base)
  45. self.s3 = bid_score(self.p3, self.base)
  46. self.gap = self.s1 - max(self.s2, self.s3)
  47. self.verdict = verdict
  48. self.comment = comment
  49. PLANS = [
  50. Plan(
  51. "方案一 顶格贴近型",
  52. "两家紧贴限价、仅差70元",
  53. "599830",
  54. "599900",
  55. "备选",
  56. "分差与我方报价同时最大化,但两家报价均贴着限价且仅差70元,配合痕迹最重",
  57. ),
  58. Plan(
  59. "方案二 分差极限型",
  60. "两家598,800 / 598,900",
  61. "598800",
  62. "598900",
  63. "备选",
  64. "分差取到结构上限2.78;数字为整百元较自然,但两家仅差100元",
  65. ),
  66. Plan(
  67. "方案三 均衡推荐型",
  68. "两家598,300 / 599,400",
  69. "598300",
  70. "599400",
  71. "★推荐",
  72. "分差2.73(上限的98%);三家均为整百元、平均值正好590,000无进位误差;价差1,100元较自然",
  73. ),
  74. Plan(
  75. "方案四 自然价差型",
  76. "两家596,800 / 599,900",
  77. "596800",
  78. "599900",
  79. "备选",
  80. "价差3,100元最自然;代价是分差少0.11分、我方少收约478元",
  81. ),
  82. Plan(
  83. "方案五 宽价差型",
  84. "两家592,000 / 598,000",
  85. "592000",
  86. "598000",
  87. "备选",
  88. "价差6,000元,外观最像独立报价;分差降至2.47,我方少收约3,679元",
  89. ),
  90. Plan(
  91. "方案六 低价拉差型",
  92. "两家452,270 / 599,400",
  93. "452270",
  94. "599400",
  95. "不建议",
  96. "分差3.00为全场最高,但我方少收约6.98万元;45万报价易被质疑异常低价或串标",
  97. ),
  98. Plan(
  99. "对照A 两家同价顶格",
  100. "两家均报600,000",
  101. "600000",
  102. "600000",
  103. "不符合要求",
  104. "理论最优解,但违反「两家不得同价、不报正好60万」的约束",
  105. ),
  106. Plan(
  107. "对照B 三家均顶格",
  108. "三家均报600,000",
  109. "600000",
  110. "600000",
  111. "不符合要求",
  112. "三家并列38.14分,我方无任何分差优势",
  113. our_price="600000",
  114. ),
  115. ]
  116. RECOMMENDED = PLANS[2]
  117. wb = Workbook()
  118. ws = wb.active
  119. ws.title = "投标行动清单"
  120. headers = ["待办项", "待办子项", "责任人", "状态", "备注"]
  121. rows = [
  122. # 一、证明材料
  123. ["一、投标证明扫描件/复印件", "营业执照扫描件/复印件", "", "待办", "需清晰完整"],
  124. ["一、投标证明扫描件/复印件", "法人身份证扫描件/复印件", "", "待办", "正反面齐全、清晰"],
  125. ["一、投标证明扫描件/复印件", "授权委托书", "", "待办", "按招标要求格式签署盖章"],
  126. ["一、投标证明扫描件/复印件", "投标函", "", "待办", "按招标文件要求填写签署"],
  127. [
  128. "一、投标证明扫描件/复印件",
  129. "(原清单第5项待明确)",
  130. "",
  131. "待补充",
  132. "原文第5项为空,需对照招标文件补全",
  133. ],
  134. [
  135. "一、投标证明扫描件/复印件",
  136. "增值税专用发票相关承诺书",
  137. "",
  138. "待办",
  139. "提供承诺书(替代直接提供专票)",
  140. ],
  141. [
  142. "一、投标证明扫描件/复印件",
  143. "2023年1月1日以来软件开发项目案例(≥20万元)—合同关键页",
  144. "",
  145. "待办",
  146. "含首页、服务内容页、金额页、签字盖章页等;框架协议业绩以2023-01-01后订单为准",
  147. ],
  148. [
  149. "一、投标证明扫描件/复印件",
  150. "上述案例对应结算发票扫描件",
  151. "",
  152. "待办",
  153. "业绩金额以结算发票金额为准",
  154. ],
  155. [
  156. "一、投标证明扫描件/复印件",
  157. "发票网上税务系统查验截图",
  158. "",
  159. "待办",
  160. "查验地址:https://inv-veri.chinatax.gov.cn/;证明材料须清晰完整",
  161. ],
  162. ["一、投标证明扫描件/复印件", "公司信用报告", "", "待办", ""],
  163. # 二、招投标网站
  164. ["二、招投标网站所需材料", "企业授权书", "", "待办", "有模板,需盖公章"],
  165. ["二、招投标网站所需材料", "CA证书购买", "", "已完成", "已购买,费用400元"],
  166. # 三(一)报价评分
  167. [
  168. "三、评标评分 /(一)报价评分",
  169. "测算评标基准价与报价策略(三人配合)",
  170. "",
  171. "待办",
  172. "最高限价60万元(含税)。评标基准价=有效应答人报价算术平均值×97%;"
  173. "≥10家去2高2低,5–9家去1高1低,3家不去极值;"
  174. "等于基准价得40分;高于每偏差1%扣0.6分,低于每偏差1%扣0.3分;四舍五入保留两位小数,最低0分。"
  175. f"推荐(含税):我方{fmt(RECOMMENDED.p1)}元得{RECOMMENDED.s1}分,"
  176. f"配合两家{fmt(RECOMMENDED.p2)}元({RECOMMENDED.s2}分)、{fmt(RECOMMENDED.p3)}元({RECOMMENDED.s3}分),"
  177. f"分差+{RECOMMENDED.gap}分。详见「报价策略测算」页。",
  178. ],
  179. [
  180. "三、评标评分 /(一)报价评分",
  181. "从备选报价方案中选定最终方案",
  182. "",
  183. "待办",
  184. f"「报价方案备选」页共列{len(PLANS)}套方案(含2套仅作对照)。"
  185. "需在分差、我方收入、报价外观自然度之间拍板;建议默认采用方案三。",
  186. ],
  187. [
  188. "三、评标评分 /(一)报价评分",
  189. "与两家配合投标人同步报价口径",
  190. "",
  191. "待办",
  192. "确认两家按选定方案报价;三家标书须独立编制,避免排版雷同、报价连号,价差建议≥1,000元",
  193. ],
  194. # 三(二)企业综合实力
  195. [
  196. "三、评标评分 /(二)企业综合实力",
  197. "软件企业证书、软件产品证书(双软)有效登记证书扫描件",
  198. "",
  199. "待办",
  200. "双软证书,提供有效登记证书扫描件",
  201. ],
  202. [
  203. "三、评标评分 /(二)企业综合实力",
  204. "2023年以来合同金额≥20万元软件开发项目业绩(最多5份)",
  205. "",
  206. "待办",
  207. "每份2分,最多10分;合同/框架协议关键页+结算发票;与第一部分案例材料可统筹准备",
  208. ],
  209. [
  210. "三、评标评分 /(二)企业综合实力",
  211. "制造业相关领域计算机软件著作权登记证书(最多9份)",
  212. "",
  213. "待办",
  214. "每份1分,最多9分;提供有效软著登记证书扫描件",
  215. ],
  216. [
  217. "三、评标评分 /(二)企业综合实力",
  218. "项目团队完整配备:项目负责人、架构师、前后端开发工程师、测试工程师、UI设计交互工程师",
  219. "",
  220. "待办",
  221. "全岗位得2分;缺1个核心岗位扣1分;以上人员均须提供与投标人签署的劳务合同,否则不得分",
  222. ],
  223. [
  224. "三、评标评分 /(二)企业综合实力",
  225. "上述项目团队成员劳务合同扫描件",
  226. "",
  227. "待办",
  228. "评分硬性条件:无劳务合同不得分",
  229. ],
  230. # 三(三)技术评分
  231. [
  232. "三、评标评分 /(三)技术评分",
  233. "项目技术需求理解及综合分析(设计蓝图)",
  234. "",
  235. "待办",
  236. "内容详实科学、合理、全面、针对性强,设计蓝图完整,可得7–9分",
  237. ],
  238. [
  239. "三、评标评分 /(三)技术评分",
  240. "演示系统功能准备",
  241. "",
  242. "待办",
  243. "覆盖产销、制造、供应、采购、物料仓储、生产执行、成品仓等核心场景,"
  244. "以及智能诊断、业务闭环处理、数据分析看板实际效果",
  245. ],
  246. [
  247. "三、评标评分 /(三)技术评分",
  248. "各子系统技术方案",
  249. "",
  250. "待办",
  251. "含功能模块设计、核心业务流程、技术架构、智能诊断集成架构、数据架构、接口设计等",
  252. ],
  253. [
  254. "三、评标评分 /(三)技术评分",
  255. "智能诊断模型设计、业务闭环自动化策略、数据分析建模技术方案 + 智能诊断Demo",
  256. "",
  257. "待办",
  258. "现场提供智能诊断 Demo 优先",
  259. ],
  260. [
  261. "三、评标评分 /(三)技术评分",
  262. "测试策略与验收方案",
  263. "",
  264. "待办",
  265. "含业务场景覆盖、诊断模型验证、用例设计、缺陷管理、验收标准;完整可得3分",
  266. ],
  267. # 三(四)实施方案
  268. [
  269. "三、评标评分 /(四)实施方案",
  270. "质量与工期保障措施方案",
  271. "",
  272. "待办",
  273. "质量控制点充分清晰、保障措施针对可行,明确承诺后续质量保障和服务,可得2分",
  274. ],
  275. [
  276. "三、评标评分 /(四)实施方案",
  277. "应急响应、漏洞修复与安全保障措施方案",
  278. "",
  279. "待办",
  280. "方案完善、保障有力、响应机制明确,可得2分",
  281. ],
  282. [
  283. "三、评标评分 /(四)实施方案",
  284. "售后服务、技术支持、响应时效及诊断模型后续优化服务承诺",
  285. "",
  286. "待办",
  287. "体系完善、含模型迭代优化承诺、响应快程度高,可得3分",
  288. ],
  289. [
  290. "三、评标评分 /(四)实施方案",
  291. "培训计划(管理员/操作员/智能诊断与数据分析专项/培训手册)",
  292. "",
  293. "待办",
  294. "计划针对性强、完善合理、满足采购人需求为优,可得3分",
  295. ],
  296. ]
  297. header_fill = PatternFill("solid", fgColor="1F4E79")
  298. header_font = Font(name="微软雅黑", bold=True, color="FFFFFF", size=11)
  299. section_fills = {
  300. "一、": PatternFill("solid", fgColor="D6EAF8"),
  301. "二、": PatternFill("solid", fgColor="D5F5E3"),
  302. "三、评标评分 /(一)": PatternFill("solid", fgColor="FCF3CF"),
  303. "三、评标评分 /(二)": PatternFill("solid", fgColor="FADBD8"),
  304. "三、评标评分 /(三)": PatternFill("solid", fgColor="E8DAEF"),
  305. "三、评标评分 /(四)": PatternFill("solid", fgColor="D6DBDF"),
  306. }
  307. status_fills = {
  308. "待办": PatternFill("solid", fgColor="FFF2CC"),
  309. "已完成": PatternFill("solid", fgColor="C6EFCE"),
  310. "待补充": PatternFill("solid", fgColor="F4B183"),
  311. "进行中": PatternFill("solid", fgColor="BDD7EE"),
  312. }
  313. thin = Border(
  314. left=Side(style="thin", color="B0B0B0"),
  315. right=Side(style="thin", color="B0B0B0"),
  316. top=Side(style="thin", color="B0B0B0"),
  317. bottom=Side(style="thin", color="B0B0B0"),
  318. )
  319. wrap = Alignment(wrap_text=True, vertical="center", horizontal="left")
  320. center = Alignment(wrap_text=True, vertical="center", horizontal="center")
  321. ws.append(headers)
  322. for col in range(1, 6):
  323. cell = ws.cell(1, col)
  324. cell.fill = header_fill
  325. cell.font = header_font
  326. cell.alignment = Alignment(wrap_text=True, vertical="center", horizontal="center")
  327. cell.border = thin
  328. ws.row_dimensions[1].height = 28
  329. ws.freeze_panes = "A2"
  330. ws.auto_filter.ref = f"A1:E{len(rows) + 1}"
  331. for i, r in enumerate(rows, start=2):
  332. ws.append(r)
  333. fill = None
  334. for prefix, pf in section_fills.items():
  335. if r[0].startswith(prefix):
  336. fill = pf
  337. break
  338. for col in range(1, 6):
  339. cell = ws.cell(i, col)
  340. cell.font = Font(name="微软雅黑", size=10)
  341. cell.border = thin
  342. cell.alignment = wrap if col in (1, 2, 5) else center
  343. if col == 1 and fill:
  344. cell.fill = fill
  345. if col == 4 and r[3] in status_fills:
  346. cell.fill = status_fills[r[3]]
  347. remark_len = len(r[4] or "")
  348. ws.row_dimensions[i].height = max(36, min(90, 22 + remark_len // 28 * 16))
  349. widths = [36, 52, 12, 10, 55]
  350. for i, w in enumerate(widths, 1):
  351. ws.column_dimensions[get_column_letter(i)].width = w
  352. dv = DataValidation(
  353. type="list",
  354. formula1='"待办,进行中,已完成,待补充,不适用"',
  355. allow_blank=True,
  356. )
  357. dv.error = "请选择预置状态"
  358. dv.errorTitle = "状态"
  359. ws.add_data_validation(dv)
  360. dv.add(f"D2:D{len(rows) + 1}")
  361. # Sheet 2: scoring reference
  362. ws2 = wb.create_sheet("评标分值速查")
  363. ws2_headers = ["评分大类", "评分项", "满分/分值区间", "得分要点"]
  364. ws2_rows = [
  365. [
  366. "(一)报价评分",
  367. "报价得分",
  368. "40分",
  369. "等于评标基准价得40分;高于基准价每偏1%扣0.6分;低于基准价每偏1%扣0.3分;最低0分",
  370. ],
  371. [
  372. "(一)报价评分",
  373. "评标基准价",
  374. "—",
  375. "有效报价算术平均×97%;≥10家去2高2低;5–9家去1高1低;3家不去极值",
  376. ],
  377. [
  378. "(一)报价评分",
  379. "项目最高限价",
  380. "60万元(含税)",
  381. "超过限价视为无效报价;税率按6%测算不含税金额",
  382. ],
  383. [
  384. "(一)报价评分",
  385. "三人配合最优策略(满分+分差最大)",
  386. "我方40分",
  387. f"含税:我方{fmt(RECOMMENDED.p1)} / 配合方{fmt(RECOMMENDED.p2)}({RECOMMENDED.s2})"
  388. f"/ 配合方{fmt(RECOMMENDED.p3)}({RECOMMENDED.s3});分差+{RECOMMENDED.gap},"
  389. "接近结构上限2.78。备选方案见「报价方案备选」页",
  390. ],
  391. ["(二)企业综合实力", "双软证书", "见招标文件", "软件企业证书+软件产品证书有效扫描件"],
  392. [
  393. "(二)企业综合实力",
  394. "软件开发业绩(≥20万,2023年以来)",
  395. "最多10分",
  396. "每份2分,最多5份;合同关键页+结算发票",
  397. ],
  398. ["(二)企业综合实力", "制造业相关软著", "最多9分", "每份1分,最多9份"],
  399. [
  400. "(二)企业综合实力",
  401. "项目团队岗位完整性",
  402. "2分",
  403. "全岗位2分;缺1岗位扣1分;须有劳务合同",
  404. ],
  405. ["(三)技术评分", "需求理解与设计蓝图", "7–9分", "详实科学、合理全面、针对性强、蓝图完整"],
  406. [
  407. "(三)技术评分",
  408. "演示系统功能",
  409. "见招标文件",
  410. "产销/制造/供应/采购/仓储/执行/成品仓+智能诊断/闭环/看板",
  411. ],
  412. ["(三)技术评分", "子系统技术方案", "见招标文件", "功能/流程/架构/诊断集成/数据/接口"],
  413. ["(三)技术评分", "智能诊断与数据分析方案", "见招标文件", "现场提供智能诊断Demo优先"],
  414. ["(三)技术评分", "测试与验收方案", "3分", "场景全面、模型验证可行、验收标准明确"],
  415. ["(四)实施方案", "质量与工期保障", "2分", "质控点充分、措施可行、承诺后续保障"],
  416. ["(四)实施方案", "应急与安全保障", "2分", "响应机制明确、保障有力"],
  417. ["(四)实施方案", "售后与模型优化服务", "3分", "含模型迭代优化承诺、响应快"],
  418. ["(四)实施方案", "培训计划", "3分", "管理员/操作员/诊断与分析专项/手册"],
  419. ]
  420. for col, h in enumerate(ws2_headers, 1):
  421. cell = ws2.cell(1, col, h)
  422. cell.fill = header_fill
  423. cell.font = header_font
  424. cell.alignment = Alignment(wrap_text=True, vertical="center", horizontal="center")
  425. cell.border = thin
  426. ws2.row_dimensions[1].height = 28
  427. ws2.freeze_panes = "A2"
  428. for i, r in enumerate(ws2_rows, start=2):
  429. for col, val in enumerate(r, 1):
  430. cell = ws2.cell(i, col, val)
  431. cell.font = Font(name="微软雅黑", size=10)
  432. cell.border = thin
  433. cell.alignment = wrap
  434. ws2.row_dimensions[i].height = 40
  435. for i, w in enumerate([22, 32, 16, 60], 1):
  436. ws2.column_dimensions[get_column_letter(i)].width = w
  437. # Sheet 3: pricing strategy
  438. ws3 = wb.create_sheet("报价策略测算")
  439. ws3_title_font = Font(name="微软雅黑", bold=True, size=14, color="1F4E79")
  440. ws3_label_font = Font(name="微软雅黑", bold=True, size=10)
  441. ws3_body_font = Font(name="微软雅黑", size=10)
  442. highlight = PatternFill("solid", fgColor="C6EFCE")
  443. warn = PatternFill("solid", fgColor="FFF2CC")
  444. ws3["A1"] = "报价策略测算(三人配合|我方满分 + 分差最大化)"
  445. ws3["A1"].font = ws3_title_font
  446. ws3.merge_cells("A1:F1")
  447. assumptions = [
  448. ("前提/约束", "内容"),
  449. ("有效投标人", "3家(含我方);不去最高/最低极值,直接算术平均"),
  450. ("最高限价", "600,000.00 元(含税)"),
  451. ("税率", "6%(增值税)"),
  452. ("评标基准价", "三家报价算术平均值 × 97%"),
  453. ("得分规则", "等于基准价40分;高于每偏1%扣0.6分;低于每偏1%扣0.3分;四舍五入两位小数,最低0分"),
  454. (
  455. "优化目标",
  456. "①我方报价分取满分40分;②与另外两家分差最大化;"
  457. "③两家配合报价不得相同、且不报正好600,000元;④我方报价尽量高",
  458. ),
  459. ]
  460. for r_idx, row in enumerate(assumptions, start=3):
  461. for c_idx, val in enumerate(row, start=1):
  462. cell = ws3.cell(r_idx, c_idx, val)
  463. cell.font = ws3_label_font if r_idx == 3 or c_idx == 1 else ws3_body_font
  464. cell.border = thin
  465. cell.alignment = wrap
  466. if r_idx == 3:
  467. cell.fill = header_fill
  468. cell.font = header_font
  469. ws3["A11"] = "推荐报价方案(最优)"
  470. ws3["A11"].font = ws3_title_font
  471. ws3.merge_cells("A11:F11")
  472. R = RECOMMENDED
  473. rec_headers = ["角色", "含税报价(元)", "不含税报价(元)", "税额(元)", "报价得分", "说明"]
  474. rec_rows = [
  475. [
  476. "我方(中标意向)",
  477. fmt(R.p1),
  478. fmt(ex_tax(R.p1)),
  479. fmt(R.p1 - ex_tax(R.p1)),
  480. str(R.s1),
  481. "恰好等于评标基准价,拿满分40分;三家合计1,770,000,平均590,000,×97%正好整数,无进位误差",
  482. ],
  483. [
  484. "配合投标人A",
  485. fmt(R.p2),
  486. fmt(ex_tax(R.p2)),
  487. fmt(R.p2 - ex_tax(R.p2)),
  488. str(R.s2),
  489. "高于基准价4.543%,扣2.73分;两家中报价较低者,决定我方分差上限",
  490. ],
  491. [
  492. "配合投标人B",
  493. fmt(R.p3),
  494. fmt(ex_tax(R.p3)),
  495. fmt(R.p3 - ex_tax(R.p3)),
  496. str(R.s3),
  497. "高于基准价4.735%,扣2.84分;与A错开1,100元,避免同价",
  498. ],
  499. [
  500. "我方分差",
  501. "—",
  502. "—",
  503. "—",
  504. f"+{R.gap}",
  505. f"{R.s1} − {max(R.s2, R.s3)}(对分数更高的配合方A);已接近结构上限2.78",
  506. ],
  507. ]
  508. for c_idx, h in enumerate(rec_headers, start=1):
  509. cell = ws3.cell(12, c_idx, h)
  510. cell.fill = header_fill
  511. cell.font = header_font
  512. cell.border = thin
  513. cell.alignment = center
  514. for r_idx, row in enumerate(rec_rows, start=13):
  515. for c_idx, val in enumerate(row, start=1):
  516. cell = ws3.cell(r_idx, c_idx, val)
  517. cell.font = ws3_body_font
  518. cell.border = thin
  519. cell.alignment = wrap if c_idx == 6 else center
  520. if r_idx == 13:
  521. cell.fill = highlight
  522. calc_headers = ["测算项", "数值", "计算说明"]
  523. calc_rows = [
  524. ["三家报价合计", "1,770,000.00", "572,300 + 598,300 + 599,400"],
  525. ["算术平均值", "590,000.00", "合计 ÷ 3(整数,无小数误差)"],
  526. ["评标基准价", "572,300.00", "590,000 × 97%"],
  527. ["我方相对基准偏差", "0%", "报价=基准价 → 40.00分(满分,不可能更高)"],
  528. ["配合方A偏差/扣分", "+4.543% / 2.73分", "(598300−572300)/572300 × 0.6"],
  529. ["配合方B偏差/扣分", "+4.735% / 2.84分", "(599400−572300)/572300 × 0.6"],
  530. ["我方分差(对A)", "+2.73分", "40.00 − 37.27,A为两家中得分较高者"],
  531. ["分差结构上限", "2.78分", "两家同价顶格时的极限值;本方案已达其98%"],
  532. ]
  533. ws3["A18"] = "关键测算过程"
  534. ws3["A18"].font = ws3_title_font
  535. for c_idx, h in enumerate(calc_headers, start=1):
  536. cell = ws3.cell(19, c_idx, h)
  537. cell.fill = header_fill
  538. cell.font = header_font
  539. cell.border = thin
  540. cell.alignment = center
  541. for r_idx, row in enumerate(calc_rows, start=20):
  542. for c_idx, val in enumerate(row, start=1):
  543. cell = ws3.cell(r_idx, c_idx, val)
  544. cell.font = ws3_body_font
  545. cell.border = thin
  546. cell.alignment = wrap
  547. ws3["A29"] = "结论与注意"
  548. ws3["A29"].font = ws3_title_font
  549. conclusions = [
  550. "1. 我方报价得分封顶40分:只要报价等于评标基准价即拿满分,不存在「更高分」,因此优化目标只能是「与另外两家的分差」。",
  551. "2. 分差存在结构上限2.78分:两家配合方报价的平均值恒为基准价的1.0464倍,"
  552. "即两家平均偏高4.639%,其中报价较低者最多被扣2.78分。两家同价时才取到该上限。",
  553. "3. 推荐方案在「两家不同价、不报正好60万」约束下取得+2.73分差(上限的98%),"
  554. "同时我方含税价572,300元、不含税539,905.66元,兼顾分差与收入。",
  555. "4. 想把分差做到2.78以上,必须让一家报到基准价以下很多(如45.2万),"
  556. "我方基准价随之被拉低到50.25万,少收约6.98万元只换0.27分,性价比极低且风险高。",
  557. "5. 三家报价须由不同主体独立编制、避免雷同排版与连号,报价差建议保持在1,000元以上。",
  558. "6. 全部备选报价方案(含2套仅作对照的不合规方案)见「报价方案备选」页,可直接选用。",
  559. "7. 若实际有效应答人不是3家,本测算全部作废,须按新家数重算(届时还涉及去极值规则)。",
  560. "8. 请以招标文件对「报价含税口径」「基准价小数位」的最终约定为准,正式填报前再核对一次。",
  561. ]
  562. for i, line in enumerate(conclusions, start=30):
  563. ws3[f"A{i}"] = line
  564. ws3[f"A{i}"].font = ws3_body_font
  565. ws3[f"A{i}"].fill = warn if i in (30, 31) else PatternFill()
  566. ws3.merge_cells(f"A{i}:H{i}")
  567. for i, w in enumerate([34, 16, 22, 16, 12, 18, 12, 46], 1):
  568. ws3.column_dimensions[get_column_letter(i)].width = w
  569. ws3.row_dimensions[1].height = 24
  570. for r in list(range(3, 11)) + list(range(13, 17)):
  571. ws3.row_dimensions[r].height = 32
  572. for r in range(30, 38):
  573. ws3.row_dimensions[r].height = 30
  574. # Sheet 4: candidate pricing plans
  575. ws5 = wb.create_sheet("报价方案备选")
  576. ws5["A1"] = "报价方案备选(三家配合|含税口径,税率6%)"
  577. ws5["A1"].font = ws3_title_font
  578. ws5.merge_cells("A1:K1")
  579. ws5["A2"] = (
  580. "我方报价分封顶40分,各方案我方均等于评标基准价拿满分,差别只在「与另外两家的分差」"
  581. "与「我方能收多少钱」。请在「选定」列用 ○ 标记最终采用的方案。"
  582. )
  583. ws5["A2"].font = ws3_body_font
  584. ws5["A2"].alignment = wrap
  585. ws5.merge_cells("A2:K2")
  586. plan_headers = [
  587. "选定",
  588. "方案",
  589. "方案要点",
  590. "角色",
  591. "含税报价(元)",
  592. "不含税报价(元)",
  593. "税额(元)",
  594. "报价得分",
  595. "我方分差",
  596. "推荐度",
  597. "说明",
  598. ]
  599. for c_idx, h in enumerate(plan_headers, start=1):
  600. cell = ws5.cell(4, c_idx, h)
  601. cell.fill = header_fill
  602. cell.font = header_font
  603. cell.border = thin
  604. cell.alignment = center
  605. verdict_fills = {
  606. "★推荐": PatternFill("solid", fgColor="C6EFCE"),
  607. "备选": PatternFill("solid", fgColor="FFF2CC"),
  608. "不建议": PatternFill("solid", fgColor="F8CBAD"),
  609. "不符合要求": PatternFill("solid", fgColor="D9D9D9"),
  610. }
  611. row_ptr = 5
  612. plan_blocks = []
  613. for plan in PLANS:
  614. start = row_ptr
  615. for role, price, sc in (
  616. ("我方", plan.p1, plan.s1),
  617. ("配合方A", plan.p2, plan.s2),
  618. ("配合方B", plan.p3, plan.s3),
  619. ):
  620. ex = ex_tax(price)
  621. values = {
  622. 4: role,
  623. 5: fmt(price),
  624. 6: fmt(ex),
  625. 7: fmt(price - ex),
  626. 8: str(sc),
  627. }
  628. for c_idx in range(1, 12):
  629. cell = ws5.cell(row_ptr, c_idx, values.get(c_idx))
  630. cell.font = ws3_body_font
  631. cell.border = thin
  632. cell.alignment = wrap if c_idx in (3, 11) else center
  633. ws5.row_dimensions[row_ptr].height = 20
  634. row_ptr += 1
  635. end = row_ptr - 1
  636. plan_blocks.append((plan, start, end))
  637. for c_idx, val in ((1, ""), (2, plan.key), (3, plan.name), (9, f"+{plan.gap}"),
  638. (10, plan.verdict), (11, plan.comment)):
  639. ws5.merge_cells(start_row=start, start_column=c_idx, end_row=end, end_column=c_idx)
  640. cell = ws5.cell(start, c_idx, val)
  641. cell.font = (
  642. Font(name="微软雅黑", size=10, bold=True)
  643. if c_idx in (2, 9, 10)
  644. else ws3_body_font
  645. )
  646. cell.alignment = wrap if c_idx in (3, 11) else center
  647. if plan.verdict in verdict_fills:
  648. cell.fill = verdict_fills[plan.verdict]
  649. dv_pick = DataValidation(type="list", formula1='"○,—"', allow_blank=True)
  650. ws5.add_data_validation(dv_pick)
  651. dv_pick.add(f"A5:A{row_ptr - 1}")
  652. summary_start = row_ptr + 1
  653. ws5.cell(summary_start, 1, "方案速览(按分差排序)").font = ws3_title_font
  654. ws5.merge_cells(start_row=summary_start, start_column=1, end_row=summary_start, end_column=11)
  655. sum_headers = ["方案", "我方含税价", "配合两家报价", "我方分差", "与推荐方案比:我方收入差", "推荐度"]
  656. for c_idx, h in enumerate(sum_headers, start=1):
  657. cell = ws5.cell(summary_start + 1, c_idx, h)
  658. cell.fill = header_fill
  659. cell.font = header_font
  660. cell.border = thin
  661. cell.alignment = center
  662. for i, plan in enumerate(sorted(PLANS, key=lambda p: (-p.gap, -p.p1)), start=summary_start + 2):
  663. diff = plan.p1 - RECOMMENDED.p1
  664. diff_txt = "基准(推荐方案)" if plan is RECOMMENDED else f"{'+' if diff >= 0 else '−'}{fmt(abs(diff))}"
  665. for c_idx, val in enumerate(
  666. [
  667. plan.key,
  668. fmt(plan.p1),
  669. f"{fmt(plan.p2)} / {fmt(plan.p3)}",
  670. f"+{plan.gap}",
  671. diff_txt,
  672. plan.verdict,
  673. ],
  674. start=1,
  675. ):
  676. cell = ws5.cell(i, c_idx, val)
  677. cell.font = ws3_body_font
  678. cell.border = thin
  679. cell.alignment = center
  680. if plan.verdict in verdict_fills:
  681. cell.fill = verdict_fills[plan.verdict]
  682. for i, w in enumerate([8, 22, 26, 12, 16, 16, 13, 11, 11, 14, 46], 1):
  683. ws5.column_dimensions[get_column_letter(i)].width = w
  684. ws5.row_dimensions[1].height = 24
  685. ws5.row_dimensions[2].height = 32
  686. ws5.freeze_panes = "A5"
  687. # Sheet 5: live score calculator (Excel formulas, recalculates on input)
  688. ws6 = wb.create_sheet("报价得分计算器")
  689. input_fill = PatternFill("solid", fgColor="FFF2CC")
  690. param_fill = PatternFill("solid", fgColor="DDEBF7")
  691. result_fill = PatternFill("solid", fgColor="E2EFDA")
  692. bold10 = Font(name="微软雅黑", size=10, bold=True)
  693. body10 = Font(name="微软雅黑", size=10)
  694. ws6["A1"] = "报价得分计算器(改动黄色单元格,得分与分差自动重算)"
  695. ws6["A1"].font = ws3_title_font
  696. ws6.merge_cells("A1:H1")
  697. ws6["A2"] = (
  698. "适用:有效应答人3家、不去极值。修改「参数」或「三家含税报价」后,"
  699. "评标基准价、各家扣分与得分、我方分差、以及「我方拿满分应报多少」全部自动更新。"
  700. )
  701. ws6["A2"].font = body10
  702. ws6["A2"].alignment = wrap
  703. ws6.merge_cells("A2:H2")
  704. def section(row, text):
  705. cell = ws6.cell(row, 1, text)
  706. cell.font = Font(name="微软雅黑", size=11, bold=True, color="1F4E79")
  707. ws6.merge_cells(start_row=row, start_column=1, end_row=row, end_column=8)
  708. def kv(row, label, value, fill=None, num_fmt=None, note=None):
  709. lab = ws6.cell(row, 1, label)
  710. lab.font = bold10
  711. lab.border = thin
  712. lab.alignment = wrap
  713. val = ws6.cell(row, 2, value)
  714. val.font = body10
  715. val.border = thin
  716. val.alignment = center
  717. if fill:
  718. val.fill = fill
  719. if num_fmt:
  720. val.number_format = num_fmt
  721. if note:
  722. cell = ws6.cell(row, 3, note)
  723. cell.font = body10
  724. cell.alignment = wrap
  725. ws6.merge_cells(start_row=row, start_column=3, end_row=row, end_column=8)
  726. section(4, "参数(一般无需改动)")
  727. kv(5, "最高限价(含税,元)", 600000, param_fill, "#,##0.00", "超过即为无效报价")
  728. kv(6, "增值税率", 0.06, param_fill, "0.0%", "用于换算不含税金额")
  729. kv(7, "评标基准价系数", 0.97, param_fill, "0%", "基准价 = 三家算术平均值 × 该系数")
  730. kv(8, "高于基准每偏1%扣分", 0.6, param_fill, "0.0", None)
  731. kv(9, "低于基准每偏1%扣分", 0.3, param_fill, "0.0", None)
  732. kv(10, "报价满分", 40, param_fill, "0", "报价=基准价时的得分,封顶值")
  733. section(12, "① 输入三家含税报价(仅需改黄色单元格)")
  734. input_headers = [
  735. "角色",
  736. "含税报价(元)",
  737. "不含税(元)",
  738. "税额(元)",
  739. "相对基准偏差",
  740. "扣分",
  741. "报价得分",
  742. "校验",
  743. ]
  744. for c_idx, h in enumerate(input_headers, start=1):
  745. cell = ws6.cell(13, c_idx, h)
  746. cell.fill = header_fill
  747. cell.font = header_font
  748. cell.border = thin
  749. cell.alignment = center
  750. for offset, (role, default) in enumerate(
  751. [("我方", 572300), ("配合方A", 598300), ("配合方B", 599400)]
  752. ):
  753. r = 14 + offset
  754. ws6.cell(r, 1, role).font = bold10
  755. ws6.cell(r, 2, default).fill = input_fill
  756. ws6.cell(r, 2).font = Font(name="微软雅黑", size=10, bold=True, color="C00000")
  757. ws6.cell(r, 3, f'=IF(B{r}="","",ROUND(B{r}/(1+$B$6),2))')
  758. ws6.cell(r, 4, f'=IF(B{r}="","",ROUND(B{r}-C{r},2))')
  759. ws6.cell(r, 5, f'=IF($B$21=0,"",(B{r}-$B$21)/$B$21)')
  760. ws6.cell(
  761. r,
  762. 6,
  763. f'=IF($B$21=0,"",ROUND(IF(B{r}>$B$21,(B{r}-$B$21)/$B$21*100*$B$8,'
  764. f'($B$21-B{r})/$B$21*100*$B$9),2))',
  765. )
  766. ws6.cell(r, 7, f'=IF($B$21=0,"",MAX(0,ROUND($B$10-F{r},2)))')
  767. ws6.cell(
  768. r,
  769. 8,
  770. f'=IF(B{r}>$B$5,"⚠ 超最高限价,报价无效",'
  771. f'IF(B{r}=$B$5,"△ 正好等于最高限价","✔ 正常"))',
  772. )
  773. for c_idx in range(1, 9):
  774. cell = ws6.cell(r, c_idx)
  775. cell.border = thin
  776. cell.alignment = center
  777. if c_idx not in (1, 2):
  778. cell.font = body10
  779. for c_idx in (2, 3, 4):
  780. ws6.cell(r, c_idx).number_format = "#,##0.00"
  781. ws6.cell(r, 5).number_format = "0.000%"
  782. ws6.cell(r, 6).number_format = "0.00"
  783. ws6.cell(r, 7).number_format = "0.00"
  784. ws6.cell(r, 7).font = bold10
  785. section(18, "② 自动测算结果")
  786. for row, label, formula, fmt_ in [
  787. (19, "三家报价合计(含税)", "=SUM(B14:B16)", "#,##0.00"),
  788. (20, "算术平均值", "=B19/3", "#,##0.00"),
  789. (21, "评标基准价", "=ROUND(B20*$B$7,2)", "#,##0.00"),
  790. (22, "我方报价得分", "=G14", "0.00"),
  791. (23, "配合两家中最高得分", "=MAX(G15:G16)", "0.00"),
  792. (24, "我方分差(对最强者)", "=ROUND(B22-B23,2)", "0.00"),
  793. (25, "我方报价分排名", "=RANK(G14,$G$14:$G$16)", "0"),
  794. ]:
  795. kv(row, label, formula, result_fill, fmt_)
  796. ws6.cell(22, 2).font = bold10
  797. ws6.cell(24, 2).font = Font(name="微软雅黑", size=11, bold=True, color="C00000")
  798. section(27, "③ 合规与外观校验")
  799. checks = [
  800. (
  801. 28,
  802. "两家是否同价",
  803. '=IF(B15=B16,"⚠ 配合两家报价相同,不满足「不得同价」要求","✔ 配合两家报价不同")',
  804. ),
  805. (
  806. 29,
  807. "是否有人正好报限价",
  808. '=IF(COUNTIF(B14:B16,$B$5)>0,"⚠ 存在正好等于最高限价的报价","✔ 无人正好报最高限价")',
  809. ),
  810. (
  811. 30,
  812. "是否存在超限价",
  813. '=IF(MAX(B14:B16)>$B$5,"⚠ 存在超过最高限价的无效报价","✔ 全部未超最高限价")',
  814. ),
  815. (
  816. 31,
  817. "我方是否踩准基准价",
  818. '=IF(B14=$B$21,"✔ 我方报价=评标基准价,已拿满分","△ 我方未踩准基准价,'
  819. '当前得分"&TEXT(G14,"0.00")&"分;应改报 "&TEXT($B$35,"#,##0.00")&" 元")',
  820. ),
  821. (
  822. 32,
  823. "配合两家价差",
  824. '=TEXT(ABS(B15-B16),"#,##0")&" 元"&IF(ABS(B15-B16)<1000,'
  825. '"(偏小,建议≥1,000元以降低配合痕迹)","(较自然)")',
  826. ),
  827. ]
  828. for row, label, formula in checks:
  829. lab = ws6.cell(row, 1, label)
  830. lab.font = bold10
  831. lab.border = thin
  832. lab.alignment = wrap
  833. cell = ws6.cell(row, 2, formula)
  834. cell.font = body10
  835. cell.border = thin
  836. cell.alignment = wrap
  837. ws6.merge_cells(start_row=row, start_column=2, end_row=row, end_column=8)
  838. section(34, "④ 反算:按上方两家报价,我方拿满分应报多少")
  839. for row, label, formula, fmt_, note in [
  840. (
  841. 35,
  842. "我方最优含税报价",
  843. "=ROUND($B$7*(B15+B16)/(3-$B$7),2)",
  844. "#,##0.00",
  845. "使我方报价恰好等于评标基准价,拿满分;把它填回上方 B14 即可",
  846. ),
  847. (36, "对应不含税金额", "=ROUND(B35/(1+$B$6),2)", "#,##0.00", None),
  848. (37, "该报价下评标基准价", "=ROUND((B35+B15+B16)/3*$B$7,2)", "#,##0.00", None),
  849. (
  850. 38,
  851. "该报价下配合方A得分",
  852. "=MAX(0,ROUND($B$10-IF(B15>$B$37,(B15-$B$37)/$B$37*100*$B$8,"
  853. "($B$37-B15)/$B$37*100*$B$9),2))",
  854. "0.00",
  855. None,
  856. ),
  857. (
  858. 39,
  859. "该报价下配合方B得分",
  860. "=MAX(0,ROUND($B$10-IF(B16>$B$37,(B16-$B$37)/$B$37*100*$B$8,"
  861. "($B$37-B16)/$B$37*100*$B$9),2))",
  862. "0.00",
  863. None,
  864. ),
  865. (40, "该报价下我方分差", "=ROUND($B$10-MAX(B38:B39),2)", "0.00", "结构上限为 2.78 分"),
  866. ]:
  867. kv(row, label, formula, result_fill, fmt_, note)
  868. ws6.cell(35, 2).font = Font(name="微软雅黑", size=11, bold=True, color="C00000")
  869. section(42, "⑤ 价差敏感度(配合方B固定为上方 B16,配合方A按不同价差下探)")
  870. sens_headers = [
  871. "两家价差(元)",
  872. "配合方A报价",
  873. "我方最优报价",
  874. "评标基准价",
  875. "A得分",
  876. "B得分",
  877. "我方分差",
  878. ]
  879. for c_idx, h in enumerate(sens_headers, start=1):
  880. cell = ws6.cell(43, c_idx, h)
  881. cell.fill = header_fill
  882. cell.font = header_font
  883. cell.border = thin
  884. cell.alignment = center
  885. for i, spread in enumerate([0, 100, 500, 1000, 2000, 3000, 5000, 8000, 12000]):
  886. r = 44 + i
  887. ws6.cell(r, 1, spread)
  888. ws6.cell(r, 2, f"=$B$16-A{r}")
  889. ws6.cell(r, 3, f"=ROUND($B$7*(B{r}+$B$16)/(3-$B$7),2)")
  890. ws6.cell(r, 4, f"=ROUND((C{r}+B{r}+$B$16)/3*$B$7,2)")
  891. ws6.cell(
  892. r,
  893. 5,
  894. f"=MAX(0,ROUND($B$10-IF(B{r}>D{r},(B{r}-D{r})/D{r}*100*$B$8,"
  895. f"(D{r}-B{r})/D{r}*100*$B$9),2))",
  896. )
  897. ws6.cell(
  898. r,
  899. 6,
  900. f"=MAX(0,ROUND($B$10-IF($B$16>D{r},($B$16-D{r})/D{r}*100*$B$8,"
  901. f"(D{r}-$B$16)/D{r}*100*$B$9),2))",
  902. )
  903. ws6.cell(r, 7, f"=ROUND($B$10-MAX(E{r}:F{r}),2)")
  904. for c_idx in range(1, 8):
  905. cell = ws6.cell(r, c_idx)
  906. cell.font = body10
  907. cell.border = thin
  908. cell.alignment = center
  909. cell.number_format = (
  910. "#,##0" if c_idx == 1 else ("#,##0.00" if c_idx <= 4 else "0.00")
  911. )
  912. ws6.cell(r, 7).font = bold10
  913. ws6.cell(54, 1, "说明:价差为0时分差取到结构上限2.78分,但两家同价不符合要求;价差越大报价越自然、分差越小。")
  914. ws6.cell(54, 1).font = body10
  915. ws6.cell(54, 1).alignment = wrap
  916. ws6.merge_cells("A54:G54")
  917. for i, w in enumerate([26, 18, 18, 16, 16, 12, 14, 30], 1):
  918. ws6.column_dimensions[get_column_letter(i)].width = w
  919. ws6.row_dimensions[1].height = 24
  920. ws6.row_dimensions[2].height = 30
  921. for r in (28, 29, 30, 31, 32, 54):
  922. ws6.row_dimensions[r].height = 26
  923. ws6.row_dimensions[35].height = 30
  924. # Sheet 6: instructions
  925. ws4 = wb.create_sheet("使用说明")
  926. notes = [
  927. "使用说明",
  928. "1. 主表「投标行动清单」按招标材料准备与评标得分项拆成可执行待办;责任人请按实际分工填写。",
  929. "2. 「状态」可下拉选择:待办 / 进行中 / 已完成 / 待补充 / 不适用。",
  930. "3. 原文「一、」第5项为空,已单列「待补充」,请对照正式招标文件补齐。",
  931. "4. 「一、」业绩案例与「三(二)」业绩加分材料可统筹准备,建议一次收集最多5份合格业绩以冲满分。",
  932. "5. 「评标分值速查」页便于对照分值;「报价策略测算」页给出推荐报价的完整推导。",
  933. "6. 「报价方案备选」页列出全部候选报价方案,每套按我方/配合方A/配合方B三行展开,"
  934. "并给出含税、不含税、税额、得分与分差;确定后在「选定」列标 ○。",
  935. "7. 「报价得分计算器」页为实时公式表:改动黄色单元格(三家含税报价)后,"
  936. "评标基准价、各家扣分与得分、我方分差、我方最优报价、价差敏感度全部自动重算。",
  937. "8. 报价测算前提:有效应答人3家、最高限价60万元(含税)、税率6%;家数变化须重算。",
  938. "9. 生成日期:2026-08-02;项目:AIDOP 投标材料准备。",
  939. ]
  940. ws4["A1"] = notes[0]
  941. ws4["A1"].font = Font(name="微软雅黑", bold=True, size=14, color="1F4E79")
  942. for i, line in enumerate(notes[1:], start=3):
  943. ws4[f"A{i}"] = line
  944. ws4[f"A{i}"].font = Font(name="微软雅黑", size=11)
  945. ws4[f"A{i}"].alignment = wrap
  946. ws4.column_dimensions["A"].width = 110
  947. out = r"d:\Projects\Ai-DOP\SourceCode\ZZYDOP\doc\AIDOP投标材料准备行动清单.xlsx"
  948. wb.save(out)
  949. print(f"saved: {out}")
  950. print(f"rows: {len(rows)}")