_unify_doc_format.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. # -*- coding: utf-8 -*-
  2. """Unify S0-S4 delivery docx front matter to S0 blueprint style.
  3. WARNING: Do not run blindly — rebuilds front matter and strips original formatting.
  4. Prefer surgical edits via _restore_cover_format.py after manual structure changes.
  5. """
  6. from __future__ import annotations
  7. from pathlib import Path
  8. from docx import Document
  9. from docx.oxml import OxmlElement
  10. from docx.shared import Pt
  11. from docx.table import Table
  12. from docx.text.paragraph import Paragraph
  13. BASE = Path(
  14. r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目管理\产互联项目管理\交付文档"
  15. )
  16. SYSTEM_NAME = "Ai-DOP智慧运营管理系统"
  17. V01, V02 = "V0.1", "V0.2"
  18. NOTE_V01, NOTE_V02 = "初版建立", "增加版本号"
  19. DOC_META = {
  20. ("S0", "req"): {
  21. "title1": "S0 基础主数据",
  22. "title2": "业务需求描述",
  23. "doc_no": "S0-BRD-001",
  24. "author": "智造易项目组",
  25. "created": "2026-06-10",
  26. "updated": "2026-06-26",
  27. },
  28. ("S0", "blue"): {
  29. "title1": "S0 基础主数据建模",
  30. "title2": "业务蓝图设计方案",
  31. "doc_no": "S0-BBP-001",
  32. "author": "智造易项目组",
  33. "created": "2026-06-10",
  34. "updated": "2026-06-26",
  35. },
  36. ("S1", "req"): {
  37. "title1": "S1 产销协同模块",
  38. "title2": "业务需求描述",
  39. "doc_no": "S1-BRD-001",
  40. "author": "彭熙玉",
  41. "created": "2026-06-09",
  42. "updated": "2026-06-09",
  43. },
  44. ("S1", "blue"): {
  45. "title1": "S1 产销协同模块",
  46. "title2": "业务蓝图设计方案",
  47. "doc_no": "S1-BBP-001",
  48. "author": "彭熙玉",
  49. "created": "2026-06-09",
  50. "updated": "2026-06-09",
  51. },
  52. ("S2", "req"): {
  53. "title1": "S2 制造协同模块",
  54. "title2": "业务需求描述",
  55. "doc_no": "S2-BRD-001",
  56. "author": "彭熙玉",
  57. "created": "2026-06-09",
  58. "updated": "2026-06-09",
  59. },
  60. ("S2", "blue"): {
  61. "title1": "S2 制造协同模块",
  62. "title2": "业务蓝图设计方案",
  63. "doc_no": "S2-BBP-001",
  64. "author": "彭熙玉",
  65. "created": "2026-06-09",
  66. "updated": "2026-06-09",
  67. },
  68. ("S3", "req"): {
  69. "title1": "S3 供应协同模块",
  70. "title2": "业务需求描述",
  71. "doc_no": "S3-BRD-001",
  72. "author": "彭熙玉",
  73. "created": "2026-06-10",
  74. "updated": "2026-06-11",
  75. },
  76. ("S3", "blue"): {
  77. "title1": "S3 供应协同模块",
  78. "title2": "业务蓝图设计方案",
  79. "doc_no": "S3-BBP-001",
  80. "author": "彭熙玉",
  81. "created": "2026-06-10",
  82. "updated": "2026-06-10",
  83. },
  84. ("S4", "req"): {
  85. "title1": "S4 采购执行模块",
  86. "title2": "业务需求描述",
  87. "doc_no": "S4-BRD-001",
  88. "author": "彭熙玉",
  89. "created": "2026-06-11",
  90. "updated": "2026-06-11",
  91. },
  92. ("S4", "blue"): {
  93. "title1": "S4 采购执行模块",
  94. "title2": "业务蓝图设计方案",
  95. "doc_no": "S4-BBP-001",
  96. "author": "彭熙玉",
  97. "created": "2026-06-11",
  98. "updated": "2026-06-11",
  99. },
  100. }
  101. REMOVE_HEADINGS = {"审核", "发布", "版本记录"}
  102. REMOVE_LINE_PREFIXES = (
  103. "文档作者:",
  104. "创建日期:",
  105. "更新日期:",
  106. "当前版本:",
  107. "文档编号:",
  108. "版本:",
  109. )
  110. def body_blocks(doc: Document):
  111. body = doc.element.body
  112. for child in body:
  113. if child.tag.endswith("p"):
  114. yield "p", Paragraph(child, doc)
  115. elif child.tag.endswith("tbl"):
  116. yield "t", Table(child, doc)
  117. def block_text(kind: str, obj) -> str:
  118. if kind == "p":
  119. return obj.text.strip()
  120. return ""
  121. def is_change_log_table(table: Table) -> bool:
  122. if not table.rows:
  123. return False
  124. header = "".join(c.text.strip() for c in table.rows[0].cells)
  125. return "变更说明" in header and ("版本" in header or "日期" in header)
  126. def is_cover_meta_table(table: Table) -> bool:
  127. if not table.rows:
  128. return False
  129. return table.rows[0].cells[0].text.strip() == "文档编号"
  130. def is_audit_or_release_table(table: Table) -> bool:
  131. if not table.rows:
  132. return False
  133. header = [c.text.strip() for c in table.rows[0].cells]
  134. return header[:3] == ["姓名", "职位", "签字/日期"] or header[:3] == ["编号", "名称", "地点"]
  135. def is_version_record_table(table: Table) -> bool:
  136. if not table.rows:
  137. return False
  138. header = [c.text.strip() for c in table.rows[0].cells]
  139. return header[:4] == ["版本", "日期", "修订人", "修订说明"]
  140. def set_cell(cell, text: str, *, bold: bool = False) -> None:
  141. cell.text = ""
  142. run = cell.paragraphs[0].add_run(text)
  143. run.font.size = Pt(10.5)
  144. run.bold = bold
  145. def build_cover_table(doc: Document, meta: dict) -> Table:
  146. rows = [
  147. ("文档编号", meta["doc_no"]),
  148. ("版本", V02),
  149. ("密级", "内部"),
  150. ("编制单位", meta["author"]),
  151. ("编制日期", meta["updated"]),
  152. ]
  153. table = doc.add_table(rows=len(rows), cols=2)
  154. for ri, (label, value) in enumerate(rows):
  155. set_cell(table.rows[ri].cells[0], label, bold=True)
  156. set_cell(table.rows[ri].cells[1], value)
  157. return table
  158. def build_change_table(doc: Document, meta: dict) -> Table:
  159. table = doc.add_table(rows=3, cols=4)
  160. headers = ["日期", "版本", "修订人", "变更说明"]
  161. data = [
  162. (meta["created"], V01, meta["author"], NOTE_V01),
  163. (meta["updated"], V02, meta["author"], NOTE_V02),
  164. ]
  165. for ci, val in enumerate(headers):
  166. set_cell(table.rows[0].cells[ci], val, bold=True)
  167. for ri, row in enumerate(data, start=1):
  168. for ci, val in enumerate(row):
  169. set_cell(table.rows[ri].cells[ci], val)
  170. return table
  171. def set_paragraph_text(p: Paragraph, text: str, style: str | None = None) -> None:
  172. p.text = text
  173. if style:
  174. p.style = style
  175. def insert_paragraph_before(ref_el, doc: Document) -> Paragraph:
  176. el = OxmlElement("w:p")
  177. ref_el.addprevious(el)
  178. return Paragraph(el, doc)
  179. def move_element_before(element, ref_el) -> None:
  180. parent = element.getparent()
  181. if parent is not None:
  182. parent.remove(element)
  183. ref_el.addprevious(element)
  184. def remove_orphan_tables_after_toc(doc: Document, toc_el) -> None:
  185. body = doc.element.body
  186. for tbl_el in list(body):
  187. if not tbl_el.tag.endswith("tbl"):
  188. continue
  189. if list(body).index(tbl_el) <= list(body).index(toc_el):
  190. continue
  191. table = Table(tbl_el, doc)
  192. if is_version_record_table(table):
  193. body.remove(tbl_el)
  194. def unify_document(path: Path, module: str, kind: str) -> None:
  195. meta = DOC_META[(module, kind)]
  196. doc = Document(path)
  197. blocks = list(body_blocks(doc))
  198. toc_idx = next(
  199. (i for i, (k, o) in enumerate(blocks) if k == "p" and block_text(k, o) == "目录"),
  200. None,
  201. )
  202. if toc_idx is None:
  203. raise RuntimeError(f"未找到目录: {path.name}")
  204. toc_el = blocks[toc_idx][1]._element
  205. for kind_b, obj in blocks[:toc_idx]:
  206. if kind_b == "p":
  207. obj._element.getparent().remove(obj._element)
  208. elif kind_b == "t":
  209. obj._tbl.getparent().remove(obj._tbl)
  210. p1 = insert_paragraph_before(toc_el, doc)
  211. p2 = insert_paragraph_before(toc_el, doc)
  212. p3 = insert_paragraph_before(toc_el, doc)
  213. move_element_before(p1._element, toc_el)
  214. move_element_before(p2._element, toc_el)
  215. move_element_before(p3._element, toc_el)
  216. set_paragraph_text(p1, meta["title1"])
  217. set_paragraph_text(p2, meta["title2"])
  218. set_paragraph_text(p3, SYSTEM_NAME)
  219. insert_paragraph_before(toc_el, doc)
  220. cover = build_cover_table(doc, meta)
  221. move_element_before(cover._tbl, toc_el)
  222. insert_paragraph_before(toc_el, doc)
  223. h1 = insert_paragraph_before(toc_el, doc)
  224. set_paragraph_text(h1, "文档控制", "Heading 1")
  225. h2 = insert_paragraph_before(toc_el, doc)
  226. set_paragraph_text(h2, "更改记录", "Heading 2")
  227. insert_paragraph_before(toc_el, doc)
  228. change = build_change_table(doc, meta)
  229. move_element_before(change._tbl, toc_el)
  230. insert_paragraph_before(toc_el, doc)
  231. remove_orphan_tables_after_toc(doc, toc_el)
  232. doc.save(path)
  233. def main() -> None:
  234. for module in ["S0", "S1", "S2", "S3", "S4"]:
  235. mod_dir = BASE / module
  236. for path in sorted(mod_dir.glob("*.docx")):
  237. kind = "blue" if "蓝图" in path.name else "req"
  238. unify_document(path, module, kind)
  239. print(f"OK {module}/{path.name}")
  240. if __name__ == "__main__":
  241. main()