_add_req_func_codes.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. # -*- coding: utf-8 -*-
  2. """Add REQ/FUNC codes from triple对照表 into existing requirement/blueprint docx."""
  3. from __future__ import annotations
  4. import re
  5. import shutil
  6. import tempfile
  7. from pathlib import Path
  8. import openpyxl
  9. from docx import Document
  10. BASE = Path(r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目管理\产互联项目管理\交付文档")
  11. DOCS = {
  12. "S0": {
  13. "req": BASE / "S0" / "S0-基础主数据-业务需求描述-V0.5.docx",
  14. "blue": BASE / "S0" / "S0-基础主数据建模-业务蓝图设计方案-V0.5.docx",
  15. "triple": BASE / "S0" / "S0-三联对照表.xlsx",
  16. },
  17. "S1": {
  18. "req": BASE / "S1" / "S1产销协同业务需求描述.docx",
  19. "blue": BASE / "S1" / "S1产销协同模块蓝图设计方案.docx",
  20. "triple": BASE / "S1" / "S1-三联对照表.xlsx",
  21. },
  22. "S2": {
  23. "req": BASE / "S2" / "S2制造协同业务需求描述.docx",
  24. "blue": BASE / "S2" / "S2制造协同模块蓝图设计方案.docx",
  25. "triple": BASE / "S2" / "S2-三联对照表.xlsx",
  26. },
  27. "S3": {
  28. "req": BASE / "S3" / "S3供应协同业务需求描述.docx",
  29. "blue": BASE / "S3" / "S3供应协同模块蓝图设计方案.docx",
  30. "triple": BASE / "S3" / "S3-三联对照表.xlsx",
  31. },
  32. "S4": {
  33. "req": BASE / "S4" / "S4采购执行业务需求描述.docx",
  34. "blue": BASE / "S4" / "S4采购执行模块蓝图设计方案.docx",
  35. "triple": BASE / "S4" / "S4-三联对照表.xlsx",
  36. },
  37. }
  38. # S3/S4 交付目录暂无 xlsx 时,与 aidopFuncCodes.ts 及 S1 编号规则对齐
  39. BUILTIN_TRIPLE: dict[str, list[tuple[str, str, str, str]]] = {
  40. "S3": [
  41. ("REQ-S3-001", "物料需求计划", "FUNC-S3-001", "物料需求计划"),
  42. ("REQ-S3-002", "物料交货计划", "FUNC-S3-002", "物料交货计划"),
  43. ("REQ-S3-003", "交货单异常记录", "FUNC-S3-003", "交货单异常记录"),
  44. ("REQ-S3-004", "物料采购申请", "FUNC-S3-004", "物料采购申请"),
  45. ("REQ-S3-005", "要货令", "FUNC-S3-005", "要货令"),
  46. ("REQ-S3-006", "物料采购订单", "FUNC-S3-006", "物料采购订单"),
  47. ("REQ-S3-007", "委外加工订单", "FUNC-S3-007", "委外加工订单"),
  48. ("REQ-S3-008", "工序外协订单", "FUNC-S3-008", "工序外协订单"),
  49. ("REQ-S3-009", "供应协同看板", "FUNC-S3-009", "供应协同看板"),
  50. ("REQ-S3-010", "工单物料齐套上线看板", "FUNC-S3-010", "工单物料齐套上线看板"),
  51. ("REQ-S3-011", "MDP运行监控", "FUNC-S3-011", "MDP运行监控"),
  52. ("REQ-S3-012", "委外采购明细", "FUNC-S3-012", "委外采购明细"),
  53. ],
  54. "S4": [
  55. ("REQ-S4-001", "供应商交货管理", "FUNC-S4-001", "供应商交货管理"),
  56. ("REQ-S4-002", "供应商发货单", "FUNC-S4-002", "供应商发货单"),
  57. ("REQ-S4-003", "采购退货单", "FUNC-S4-003", "采购退货单"),
  58. ("REQ-S4-004", "IQC退货查询", "FUNC-S4-004", "IQC退货查询"),
  59. ("REQ-S4-005", "采购执行看板主页", "FUNC-S4-005", "采购执行看板主页"),
  60. ("REQ-S4-006", "供应商欠料看板", "FUNC-S4-006", "供应商欠料看板"),
  61. ],
  62. }
  63. # Document title aliases -> triple 需求名称 key
  64. S0_ALIASES = {
  65. "物料维护": "物料管理",
  66. "客户信息维护": "客户管理",
  67. "订单优先级配置": "订单优先级规则管理",
  68. "合同评审周期": "合同评审周期规则管理",
  69. "产品设计周期": "产品设计周期规则管理",
  70. "订单评审周期": "订单评审周期规则管理",
  71. "供应商维护": "供应商管理",
  72. "货源清单": "采购分类与货源管理",
  73. "品类采购前置期": "品类交货周期管理",
  74. "物料计划周期": "物料计划周期管理",
  75. "物料计划周期标准": "物料计划周期管理",
  76. "生产线维护": "产线管理",
  77. "生产线": "产线管理",
  78. "工作中心": "工作中心管理",
  79. "产线岗位维护": "产线工位管理",
  80. "生产线物料(线边物料)": "产线物料管理",
  81. "线边物料": "产线物料管理",
  82. "人员技能维护": "人员技能分配",
  83. "标准工序": "标准工序管理",
  84. "标准工艺路线": "标准工艺路线管理",
  85. "工序流转卡": "工艺流程卡管理",
  86. "标准 BOM": "标准 BOM 管理",
  87. "SOP 维护": "SOP 维护管理",
  88. "SOP 文档维护": "SOP 维护管理",
  89. "SOP 文件类型": "SOP 文件类型管理",
  90. "前处理要素": "前置工序要素管理",
  91. "物料前处理要素": "前置工序要素管理",
  92. "前处理要素参数": "前置工序要素参数管理",
  93. "生产要素参数": "生产要素参数管理",
  94. "物料替代": "物料替代明细管理",
  95. "物料替代关系": "物料替代明细管理",
  96. "物料替代(制造入口)": "物料替代明细管理(制造域)",
  97. "选择替代方案": "物料代替方案管理",
  98. "选择替代方案(制造入口)": "物料代替方案管理",
  99. "工单控制参数": "工单控制参数管理",
  100. "订单排程周期": "订单排程周期管理",
  101. "检验项目": "检验项目管理",
  102. "检验方法": "检验方法管理",
  103. "检验依据": "检验基准管理",
  104. "检验标准": "检验标准管理",
  105. "检验频率": "检验频次管理",
  106. "检验仪器": "检验仪器管理",
  107. "抽样方案": "抽样方案管理",
  108. "原材料检验规范(IQC)": "来料检验规范(IQC)",
  109. "过程检验规范(IPQC)": "过程检验规范(IPQC)",
  110. "FQC 检验规范": "成品检验规范(OQC/FQC)",
  111. "OQC 检验规范": "成品检验规范(OQC/FQC)",
  112. "原材料白名单": "来料白名单管理",
  113. "质量基础": "质量字典管理",
  114. "质量基础字典": "质量字典管理",
  115. "库位维护": "库位管理",
  116. "货架列表": "库位货架管理",
  117. "部门维护": "部门管理",
  118. "雇员列表": "员工管理",
  119. "物料职责维护": "员工工作职责管理",
  120. "成本中心": "成本中心管理",
  121. "零件包装规格": "物料包装规格管理",
  122. "物料状态任务指派": "任务分配管理",
  123. "人员技能": "人员技能管理",
  124. "标签格式": "标签类型管理",
  125. "单号类型": "批次类型管理",
  126. "单号规则维护": "批次号控制管理",
  127. "单号规则": "批次号控制管理",
  128. }
  129. S1_ALIASES = {}
  130. S2_ALIASES = {
  131. "生产排程": "生产排程",
  132. "排产异常记录": "排产异常记录",
  133. "排产异常": "排产异常记录",
  134. "排产数据维护": "排产异常记录",
  135. "可执行日计划": "可执行日计划",
  136. "产线工作日历管理": "产线工作日历管理",
  137. "产线工作日历": "产线工作日历管理",
  138. "产线休息时间管理": "产线休息时间管理",
  139. "产线休息时间": "产线休息时间管理",
  140. "产线节假日管理": "产线节假日管理",
  141. "产线节假日": "产线节假日管理",
  142. "产线加班管理": "产线加班管理",
  143. "产线加班": "产线加班管理",
  144. "工单执行进度看板": "工单执行进度看板",
  145. "工单执行进度": "工单执行进度看板",
  146. "工单执行追踪": "工单执行进度看板",
  147. "S2制造协同看板": "S2制造协同看板(KPI)",
  148. "制造协同看板与KPI": "S2制造协同看板(KPI)",
  149. "制造协同看板": "S2制造协同看板(KPI)",
  150. "作业计划": "可执行日计划",
  151. }
  152. S1_REQ_SECTION = {
  153. "产品设计管理": "REQ-S1-001",
  154. "合同评审管理": "REQ-S1-002",
  155. "订单评审管理": "REQ-S1-003",
  156. "订单交付管理": "REQ-S1-004",
  157. "订单发货管理": "REQ-S1-005",
  158. "工单下达": "REQ-S1-006",
  159. "计划联动看板": "REQ-S1-007",
  160. "需求明细核验": "REQ-S1-008",
  161. "产销协同看板": "REQ-S1-009",
  162. }
  163. S1_FUNC_SECTION = {k: f"FUNC-S1-{i:03d}" for i, k in enumerate(
  164. ["产品设计管理", "订单评审管理", "订单交付管理", "订单发货管理", "合同评审管理",
  165. "计划联动看板", "需求明细核验", "工单下达", "产销协同看板"], 1)}
  166. # S1 blueprint chapter order differs from REQ order; map by title
  167. S1_FUNC_BY_TITLE = {
  168. "产品设计管理": "FUNC-S1-001",
  169. "订单评审管理": "FUNC-S1-003",
  170. "订单交付管理": "FUNC-S1-004",
  171. "订单发货管理": "FUNC-S1-005",
  172. "合同评审管理": "FUNC-S1-002",
  173. "计划联动看板": "FUNC-S1-007",
  174. "需求明细核验": "FUNC-S1-008",
  175. "工单下达": "FUNC-S1-006",
  176. "产销协同看板": "FUNC-S1-009",
  177. }
  178. S3_REQ_SECTION = {name: req for req, name, _, _ in BUILTIN_TRIPLE["S3"]}
  179. S3_FUNC_BY_TITLE = {
  180. title: func
  181. for _, name, func, fname in BUILTIN_TRIPLE["S3"]
  182. for title in (name, fname)
  183. }
  184. S4_REQ_SECTION = {name: req for req, name, _, _ in BUILTIN_TRIPLE["S4"]}
  185. S4_FUNC_BY_TITLE = {
  186. title: func
  187. for _, name, func, fname in BUILTIN_TRIPLE["S4"]
  188. for title in (name, fname)
  189. }
  190. S3_ALIASES: dict[str, str] = {}
  191. S4_ALIASES: dict[str, str] = {}
  192. def load_triple_from_rows(rows: list[tuple[str, str, str, str]]) -> dict[str, tuple[str, str]]:
  193. by_name: dict[str, tuple[str, str]] = {}
  194. for req, req_name, func, func_name in rows:
  195. by_name[req_name] = (req, func)
  196. by_name[func_name] = (req, func)
  197. return by_name
  198. def load_triple(path: Path, module: str) -> dict[str, tuple[str, str]]:
  199. if module in BUILTIN_TRIPLE and not path.exists():
  200. return load_triple_from_rows(BUILTIN_TRIPLE[module])
  201. tmp = Path(tempfile.gettempdir()) / f"{module}_triple.xlsx"
  202. shutil.copy2(path, tmp)
  203. wb = openpyxl.load_workbook(tmp, data_only=True)
  204. ws = wb.active
  205. by_name: dict[str, tuple[str, str]] = {}
  206. if module == "S2":
  207. for row in ws.iter_rows(min_row=2, max_row=ws.max_row, values_only=True):
  208. if not row or len(row) < 5:
  209. continue
  210. func, req = str(row[3]).strip(), str(row[4]).strip()
  211. if not req.startswith("REQ-"):
  212. continue
  213. menu = str(row[6] or "")
  214. name = menu.split("→")[-1].strip() if "→" in menu else func
  215. by_name[name] = (req, func)
  216. # also map by standard func names from repo
  217. names = [
  218. "生产排程", "排产异常记录", "可执行日计划", "产线工作日历管理", "产线休息时间管理",
  219. "产线节假日管理", "产线加班管理", "工单执行进度看板", "S2制造协同看板(KPI)",
  220. ]
  221. reqs = [f"REQ-S2-{i:03d}" for i in range(1, 10)]
  222. funcs = [f"FUNC-S2-{i:03d}" for i in range(1, 10)]
  223. for n, r, f in zip(names, reqs, funcs):
  224. by_name.setdefault(n, (r, f))
  225. wb.close()
  226. return by_name
  227. for row in ws.iter_rows(min_row=2, max_row=ws.max_row, values_only=True):
  228. if not row or not row[0]:
  229. continue
  230. req, req_name, func, func_name = [str(x).strip() for x in row[:4]]
  231. by_name[req_name] = (req, func)
  232. by_name[func_name] = (req, func)
  233. wb.close()
  234. return by_name
  235. def build_title_map(module: str, by_name: dict[str, tuple[str, str]]) -> dict[str, tuple[str, str]]:
  236. aliases = {"S0": S0_ALIASES, "S1": S1_ALIASES, "S2": S2_ALIASES, "S3": S3_ALIASES, "S4": S4_ALIASES}[module]
  237. title_map: dict[str, tuple[str, str]] = {}
  238. for name, codes in by_name.items():
  239. title_map[name] = codes
  240. for alias, target in aliases.items():
  241. if target in by_name:
  242. title_map[alias] = by_name[target]
  243. elif target in title_map:
  244. title_map[alias] = title_map[target]
  245. return title_map
  246. def normalize_title(text: str) -> str:
  247. text = text.strip()
  248. text = re.sub(r"^([一二三四五六七八九十]+)", "", text)
  249. text = re.sub(r"^\d+(?:\.\d+)*\s+", "", text)
  250. text = re.sub(r"\d+$", "", text).strip()
  251. text = re.sub(r"\s*——.*$", "", text)
  252. text = re.sub(r"\s*—\s*.*$", "", text)
  253. text = re.sub(r"(FUNC-[^)]+)", "", text)
  254. text = re.sub(r"(REQ-[^)]+)", "", text)
  255. text = re.sub(r"\s*\(REQ-[^)]+\)", "", text)
  256. text = re.sub(r"\s*\(FUNC-[^)]+\)", "", text)
  257. return text.strip()
  258. def extract_heading_title(text: str) -> str | None:
  259. text = text.strip()
  260. if not text:
  261. return None
  262. if text in {"功能说明", "业务描述", "目录", "目录2"}:
  263. return None
  264. if re.match(r"^[一二三四五六七八九十]+、", text):
  265. return None
  266. if " — " in text or " — " in text:
  267. return normalize_title(text.split("—", 1)[0])
  268. if len(text) > 80:
  269. return None
  270. m = re.match(r"^([一二三四五六七八九十]+)(.+)$", text)
  271. if m:
  272. return normalize_title(m.group(1))
  273. m = re.match(r"^(\d+(?:\.\d+)*)\s+(.+)$", text)
  274. if m:
  275. return normalize_title(m.group(2))
  276. if len(text) <= 40:
  277. return normalize_title(text)
  278. return None
  279. def has_code(text: str, code: str) -> bool:
  280. return code in text
  281. def set_paragraph_text(paragraph, new_text: str) -> None:
  282. if paragraph.runs:
  283. paragraph.runs[0].text = new_text
  284. for r in paragraph.runs[1:]:
  285. r.text = ""
  286. else:
  287. paragraph.add_run(new_text)
  288. def strip_existing_codes(text: str) -> str:
  289. return re.sub(r"((?:REQ|FUNC)-S\d+-\d+)", "", text)
  290. def append_code(paragraph, code: str) -> bool:
  291. text = paragraph.text
  292. if has_code(text, code):
  293. return False
  294. suffix = f"({code})"
  295. set_paragraph_text(paragraph, text.rstrip() + suffix)
  296. return True
  297. def match_codes(module: str, title: str, title_map: dict[str, tuple[str, str]], doc_kind: str) -> str | None:
  298. if module == "S1" and doc_kind == "req":
  299. return S1_REQ_SECTION.get(title)
  300. if module == "S1" and doc_kind == "blue":
  301. return S1_FUNC_BY_TITLE.get(title)
  302. if module == "S3" and doc_kind == "req":
  303. return S3_REQ_SECTION.get(title)
  304. if module == "S3" and doc_kind == "blue":
  305. return S3_FUNC_BY_TITLE.get(title)
  306. if module == "S4" and doc_kind == "req":
  307. return S4_REQ_SECTION.get(title)
  308. if module == "S4" and doc_kind == "blue":
  309. return S4_FUNC_BY_TITLE.get(title)
  310. if title in title_map:
  311. req, func = title_map[title]
  312. return req if doc_kind == "req" else func
  313. return None
  314. def iter_paragraphs(doc: Document):
  315. for p in doc.paragraphs:
  316. yield p
  317. for table in doc.tables:
  318. for row in table.rows:
  319. for cell in row.cells:
  320. for p in cell.paragraphs:
  321. yield p
  322. S2_BLUE_EXACT_ROWS = {
  323. "生产排程": "FUNC-S2-001",
  324. "排产异常记录": "FUNC-S2-002",
  325. "排产数据维护": "FUNC-S2-002",
  326. "可执行日计划": "FUNC-S2-003",
  327. "日计划列表查询": "FUNC-S2-003",
  328. "日计划下达": "FUNC-S2-003",
  329. "产线工作日历管理": "FUNC-S2-004",
  330. "产线休息时间管理": "FUNC-S2-005",
  331. "产线节假日管理": "FUNC-S2-006",
  332. "产线加班管理": "FUNC-S2-007",
  333. "工单执行进度看板": "FUNC-S2-008",
  334. "工单执行追踪": "FUNC-S2-008",
  335. }
  336. def s2_req_inline_match(text: str, key: str) -> bool:
  337. if key == "排产异常":
  338. return text.startswith("排产异常")
  339. if key == "可执行日计划":
  340. return text.startswith("展示面向车间产线的可执行日计划") or text.startswith('计划员/调度人员进入"可执行日计划"')
  341. if key == "产线工作日历":
  342. return text.startswith("产线工作日历")
  343. if key == "产线休息时间":
  344. return text.startswith("产线休息时间")
  345. if key == "产线节假日":
  346. return text.startswith("产线节假日")
  347. if key == "产线加班":
  348. return text.startswith("产线加班")
  349. if key == "工单执行进度":
  350. return text.startswith("工单执行进度") or text.startswith("管理层/计划员进入\"工单执行进度看板\"")
  351. return False
  352. def process_doc(
  353. path: Path,
  354. module: str,
  355. title_map: dict[str, tuple[str, str]],
  356. doc_kind: str,
  357. *,
  358. refresh: bool = False,
  359. ) -> int:
  360. doc = Document(path)
  361. changed = 0
  362. if refresh:
  363. for p in iter_paragraphs(doc):
  364. stripped = strip_existing_codes(p.text)
  365. if stripped != p.text:
  366. set_paragraph_text(p, stripped)
  367. changed += 1
  368. for p in iter_paragraphs(doc):
  369. text = p.text.strip()
  370. if not text:
  371. continue
  372. if module == "S2" and doc_kind == "blue" and text in S2_BLUE_EXACT_ROWS:
  373. if append_code(p, S2_BLUE_EXACT_ROWS[text]):
  374. changed += 1
  375. continue
  376. title = extract_heading_title(p.text)
  377. if not title:
  378. continue
  379. code = match_codes(module, title, title_map, doc_kind)
  380. if not code:
  381. continue
  382. if append_code(p, code):
  383. changed += 1
  384. if module == "S2" and doc_kind == "req":
  385. inline_keys = [
  386. ("排产异常", "REQ-S2-002"),
  387. ("可执行日计划", "REQ-S2-003"),
  388. ("产线工作日历", "REQ-S2-004"),
  389. ("产线休息时间", "REQ-S2-005"),
  390. ("产线节假日", "REQ-S2-006"),
  391. ("产线加班", "REQ-S2-007"),
  392. ("工单执行进度", "REQ-S2-008"),
  393. ]
  394. for p in iter_paragraphs(doc):
  395. t = p.text.strip()
  396. if not t or len(t) > 200:
  397. continue
  398. for key, code in inline_keys:
  399. if s2_req_inline_match(t, key) and not has_code(t, code):
  400. if append_code(p, code):
  401. changed += 1
  402. break
  403. doc.save(path)
  404. return changed
  405. def main():
  406. import sys
  407. refresh = "--refresh" in sys.argv
  408. only = None
  409. for arg in sys.argv[1:]:
  410. if arg.startswith("--only="):
  411. only = {x.strip().upper() for x in arg.split("=", 1)[1].split(",") if x.strip()}
  412. modules = [m for m in DOCS if not only or m in only]
  413. for module in modules:
  414. cfg = DOCS[module]
  415. by_name = load_triple(cfg["triple"], module)
  416. title_map = build_title_map(module, by_name)
  417. n1 = process_doc(cfg["req"], module, title_map, "req", refresh=refresh)
  418. n2 = process_doc(cfg["blue"], module, title_map, "blue", refresh=refresh)
  419. print(f"{module}: req +{n1}, blue +{n2}, mapped titles {len(title_map)}")
  420. if __name__ == "__main__":
  421. main()