| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- # -*- 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()
|