generate_manual_fill_workbook.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. """生成「人工补填清单」Excel:列出所有非 DMS 来源的模板字段(需人工搬运/填写)。
  2. 来源分类沿用 scripts/generate_dms_mapping.py:
  3. - DMS来源 —— 自动从 DMS 模型拉取,无需人工
  4. - 源Excel搬运(DMS未建模型)—— 源 Excel 有、DMS 无模型,需人工从源文件搬运
  5. - 新增融合 —— DMS 与源 Excel 均无,为图谱建模/关联添加,需人工确认填写
  6. 输出:data/manual_fill/人工补填清单.xlsx
  7. """
  8. from __future__ import annotations
  9. import importlib.util
  10. import re
  11. import sys
  12. from pathlib import Path
  13. from openpyxl import Workbook, load_workbook
  14. from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
  15. from openpyxl.utils import get_column_letter
  16. sys.stdout.reconfigure(encoding="utf-8")
  17. ROOT = Path(r"E:\CODE\knowledge_agent")
  18. # 载入字段分类模块(复用 TEMPLATES / FUSION_FIELDS / SOURCE_EXCEL_NOTES / OVERRIDES / EXCLUDE_DMS)
  19. _spec = importlib.util.spec_from_file_location(
  20. "dms_map", ROOT / "scripts" / "generate_dms_mapping.py"
  21. )
  22. dms_map = importlib.util.module_from_spec(_spec)
  23. assert _spec and _spec.loader
  24. _spec.loader.exec_module(dms_map)
  25. TEMPLATES = dms_map.TEMPLATES
  26. FUSION = dms_map.FUSION_FIELDS
  27. SRC_NOTES = dms_map.SOURCE_EXCEL_NOTES
  28. DEFAULT_SOURCES = dms_map.DEFAULT_SOURCES
  29. OVERRIDES = dms_map.OVERRIDES
  30. EXCLUDE_DMS = dms_map.EXCLUDE_DMS
  31. NOTES = dms_map.NOTES
  32. ALIASES = dms_map.load_dms()
  33. # 每个模板与 DMS 数据合并时的对齐键(决定 DMS 行与人工补填行如何 merge)
  34. MERGE_KEYS: dict[str, str] = {
  35. "项目信息": "项目编号",
  36. "项目月度财务": "项目编号 + 年月",
  37. "投标记录": "项目编号 + 年份",
  38. "人员信息": "工号",
  39. "考勤与人员财务": "工号 + 项目编号 + 年月",
  40. "排班明细": "工号 + 项目编号 + 年月",
  41. "岗位编制": "项目编号 + 岗位",
  42. "设备信息": "项目编号 + 设备编号",
  43. "采购与维保": "项目编号 + 名称/合同名称 + 月份/日期",
  44. "检查记录": "项目编号 + 检查日期",
  45. "片区信息": "片区编号",
  46. "项目片区关系": "项目编号 + 片区编号",
  47. "人员证书": "工号 + 证书名称",
  48. }
  49. # 特殊备注(源数据坑 / 填写口径提醒)
  50. SPECIAL_NOTES: dict[tuple[str, str], str] = {
  51. ("考勤与人员财务", "税后工资"): "源表无「税后工资」列名:赵巷公园等文件该金额填在表头为「员工签字」的列(如 4896.25)",
  52. ("考勤与人员财务", "1-31日"): "值为 常/日/夜/日1 等班次标记,原样搬运",
  53. ("排班明细", "1-31日班次"): "值为班次代码,如 OFF/A003/SQ-104/M11-033;OFF=休息",
  54. ("人员证书", "姓名"): "可读性核对列,由工号解析;登记表申勤 sheet 缺工号,需按姓名在花名册补全",
  55. ("人员信息", "当前服务项目编号"): "DMS sq_employee 仅有项目名称(五级公司),编号需人工映射为项目编号",
  56. ("项目信息", "项目负责人工号"): "DMS 有项目负责人姓名,工号需在员工花名册按姓名匹配",
  57. ("设备信息", "设备责任人工号"): "模板必填;源设备台账可能有责任人姓名,工号需匹配花名册",
  58. ("项目信息", "续签前项目编号"): "可从 scripts/analyze_renewal_folders.py 生成的 output/续签链-档案目录.csv 回填",
  59. ("项目信息", "上级项目编号"): "由业务大表层级序号/委托方式推导父项目",
  60. ("项目月度财务", "财务经办人工号"): "源收费进程表有经办人姓名,工号需匹配花名册",
  61. }
  62. def norm_header(h: str, tpl: str) -> str:
  63. """模板表头 → 规范字段名(与 generate_dms_mapping.TEMPLATES 对齐)。"""
  64. h = h.strip()
  65. h = re.sub(r"\*$", "", h)
  66. h = re.sub(r"(选填)$", "", h)
  67. m = re.match(r"^(\d{1,2})日$", h)
  68. if m:
  69. return "1-31日班次" if tpl == "排班明细" else "1-31日"
  70. h = re.sub(r"([^)]*)$", "", h)
  71. return h
  72. def dms_src(tpl: str, f: str) -> str:
  73. """判断字段是否有 DMS 来源(与 generate_dms_mapping 同逻辑)。"""
  74. ov = OVERRIDES.get((tpl, f))
  75. if ov:
  76. return f"{ov[0]}.{ov[1]}"
  77. if (tpl, f) in EXCLUDE_DMS:
  78. return ""
  79. for alias, desc in ALIASES.items():
  80. nf = dms_map.norm(f)
  81. if nf == alias or (len(nf) >= 2 and nf in alias):
  82. return desc
  83. return ""
  84. def read_template_meta() -> dict[str, set[str]]:
  85. """读取 data/templates/*.xlsx:返回 {模板: 必填字段集合}。"""
  86. required: dict[str, set[str]] = {}
  87. for f in sorted((ROOT / "data" / "templates").glob("*.xlsx")):
  88. tpl = f.stem
  89. wb = load_workbook(f, read_only=True)
  90. ws = wb.worksheets[0]
  91. row = next(ws.iter_rows(min_row=1, max_row=1))
  92. req: set[str] = set()
  93. for c in row:
  94. try:
  95. rgb = str(c.fill.fgColor.rgb) if c.fill and c.fill.fgColor else ""
  96. except Exception: # noqa: BLE001
  97. rgb = ""
  98. if "C00000" in rgb and c.value:
  99. req.add(norm_header(str(c.value), tpl))
  100. required[tpl] = req
  101. wb.close()
  102. return required
  103. HEADER_FILL = PatternFill("solid", fgColor="2F5597")
  104. SRC_FILL = PatternFill("solid", fgColor="EDEDED")
  105. FUSION_FILL = PatternFill("solid", fgColor="FFF2CC")
  106. REQ_FILL = PatternFill("solid", fgColor="FCE4D6")
  107. HEADER_FONT = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
  108. BODY_FONT = Font(name="微软雅黑", size=10)
  109. NOTE_FONT = Font(name="微软雅黑", size=9, color="595959")
  110. THIN = Side(style="thin", color="BFBFBF")
  111. BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
  112. def main() -> None:
  113. required = read_template_meta()
  114. out_dir = ROOT / "data" / "manual_fill"
  115. out_dir.mkdir(parents=True, exist_ok=True)
  116. out_path = out_dir / "人工补填清单.xlsx"
  117. wb = Workbook()
  118. ws = wb.active
  119. ws.title = "补填清单"
  120. headers = ["模板", "序号", "字段", "补填类型", "来源与填写说明", "与DMS对齐键", "必填", "备注/示例"]
  121. widths = [16, 6, 20, 16, 70, 26, 6, 46]
  122. for c, (h, w) in enumerate(zip(headers, widths), start=1):
  123. cell = ws.cell(row=1, column=c, value=h)
  124. cell.font = HEADER_FONT
  125. cell.fill = HEADER_FILL
  126. cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
  127. cell.border = BORDER
  128. ws.column_dimensions[get_column_letter(c)].width = w
  129. ws.row_dimensions[1].height = 28
  130. total_manual = 0
  131. total_fusion = 0
  132. total_src = 0
  133. r = 2
  134. per_tpl_summary: list[tuple[str, int, int, int]] = []
  135. for tpl, fields in TEMPLATES.items():
  136. req_set = required.get(tpl, set())
  137. manual = []
  138. for f in fields:
  139. if not dms_src(tpl, f):
  140. manual.append(f)
  141. n_fusion = sum(1 for f in manual if f in FUSION.get(tpl, set()))
  142. n_src = len(manual) - n_fusion
  143. total_manual += len(manual)
  144. total_fusion += n_fusion
  145. total_src += n_src
  146. per_tpl_summary.append((tpl, len(fields), len(manual), n_fusion))
  147. for i, f in enumerate(manual, start=1):
  148. if f in FUSION.get(tpl, set()):
  149. kind = "新增融合"
  150. note = "DMS 与源 Excel 均无,需人工确认填写;用途:图谱关联/可读性核对"
  151. else:
  152. kind = "源Excel搬运"
  153. note = SRC_NOTES.get((tpl, f)) or DEFAULT_SOURCES.get(tpl, "")
  154. ws.cell(row=r, column=1, value=tpl).font = BODY_FONT
  155. ws.cell(row=r, column=2, value=i).font = BODY_FONT
  156. ws.cell(row=r, column=3, value=f).font = BODY_FONT
  157. ws.cell(row=r, column=4, value=kind).font = BODY_FONT
  158. ws.cell(row=r, column=5, value=note).font = BODY_FONT
  159. ws.cell(row=r, column=6, value=MERGE_KEYS.get(tpl, "")).font = BODY_FONT
  160. is_req = "是" if f in req_set else "否"
  161. ws.cell(row=r, column=7, value=is_req).font = BODY_FONT
  162. ws.cell(row=r, column=8, value=SPECIAL_NOTES.get((tpl, f), "")).font = BODY_FONT
  163. ws.cell(row=r, column=4).fill = FUSION_FILL if kind == "新增融合" else SRC_FILL
  164. if is_req == "是":
  165. ws.cell(row=r, column=7).fill = REQ_FILL
  166. for c in range(1, 9):
  167. ws.cell(row=r, column=c).border = BORDER
  168. ws.cell(row=r, column=c).alignment = Alignment(vertical="top", wrap_text=True)
  169. r += 1
  170. ws.freeze_panes = "A2"
  171. ws.auto_filter.ref = f"A1:H{r - 1}"
  172. # ---- 使用说明 sheet ----
  173. ns = wb.create_sheet("使用说明")
  174. ns.column_dimensions["A"].width = 118
  175. lines = [
  176. "【用途】DMS 数据 + 人工补填 → 13 类模板样式 → 知识图谱构建",
  177. "",
  178. "一、字段来源三类",
  179. " ① DMS来源:DMS 模型中有对应字段(见 docs/DMS字段映射.md),由系统自动拉取,无需人工填写。",
  180. " ② 源Excel搬运:源 Excel 中有、但 DMS 未建模型(考勤/排班/设备/检查/月度财务等),需人工从源文件搬运到模板。",
  181. " ③ 新增融合:DMS 与源 Excel 均无,为图谱关联/可读性添加,需人工确认填写(工号、项目编号、片区编号等)。",
  182. "",
  183. "二、合并流程(如何从 DMS + 人工数据得到模板)",
  184. " 1. DMS 拉取:按 docs/DMS字段映射.md 的映射,从 16 个申勤模型拉取 DMS来源 字段(scripts/fetch_dms_fields.py 已打通模型定义;",
  185. " 拉数需有效 token,token 过期时从 DMS 控制台重新复制 vuejs_token)。",
  186. " 2. 人工补填:按本清单「补填清单」sheet 逐字段处理——源Excel搬运 = 从源文件复制对应列;新增融合 = 人工确认填写。",
  187. " 实际数据填到 data/templates 下 13 份模板文件(每份含填写说明);也可按本清单建立录入任务分发给各部门。",
  188. " 3. 合并:以「与DMS对齐键」为键,将 DMS 数据行与人工补填数据行合并成完整模板行(键相同的行合并,人工列缺失则报错)。",
  189. " 4. 校验:graph/reader 字段名写死校验 + 必填校验 + 引用存在性校验(缺项目/人员/片区/经办人/责任人会给出警告清单)。",
  190. " 5. 构建:scripts/build_graph.py 读取模板文件 → Neo4j 两阶段构建(先节点后关系)。",
  191. "",
  192. "三、各模板对齐键",
  193. ]
  194. for tpl, _, _, _ in per_tpl_summary:
  195. lines.append(f" {tpl}:{MERGE_KEYS[tpl]}")
  196. lines += [
  197. "",
  198. "四、本次需要人工补填的规模",
  199. f" 13 类模板共 {sum(n for _, n, _, _ in per_tpl_summary)} 个字段;DMS 自动提供 {sum(n for _, n, m, _ in per_tpl_summary) - total_manual} 个;",
  200. f" 需人工补填 {total_manual} 个(其中 源Excel搬运 {total_src} 个、新增融合 {total_fusion} 个)。",
  201. "",
  202. "五、特殊说明",
  203. " · 考勤「税后工资」:源考勤表没有该列名,赵巷公园等文件的月度工资额填在表头为「员工签字」的列(如 4896.25),按此口径搬运。",
  204. " · 考勤/排班「备注」:考勤备注来自考勤表「备注」列;排班文件无备注列,标为新增融合。",
  205. " · 「1-31日」「1-31日班次」在模板中是 31 列(1日~31日),本清单合并为一行表示。",
  206. " · 凡涉及人的字段必须补工号(项目负责人/检查人/投标/采购/财务经办人/设备责任人/片区负责人),避免重名。",
  207. " · 续签前项目编号、上级项目编号可由 scripts/analyze_renewal_folders.py、业务大表推导后回填,人工只需核对。",
  208. "",
  209. "六、相关文件",
  210. " 字段映射(含 DMS 字段):docs/DMS字段映射.md、output/dms_template_mapping.csv",
  211. " 13 份数据模板:data/templates/*.xlsx",
  212. " 本清单:data/manual_fill/人工补填清单.xlsx",
  213. ]
  214. ns.cell(row=1, column=1, value=lines[0]).font = Font(name="微软雅黑", size=12, bold=True)
  215. for i, n in enumerate(lines[1:], start=2):
  216. cell = ns.cell(row=i, column=1, value=n)
  217. cell.font = NOTE_FONT
  218. cell.alignment = Alignment(wrap_text=True, vertical="top")
  219. wb.save(out_path)
  220. print("输出:", out_path)
  221. print(f"需人工补填字段总数: {total_manual}(源Excel搬运 {total_src} / 新增融合 {total_fusion})")
  222. for tpl, n, m, nf in per_tpl_summary:
  223. print(f" {tpl}: 共{n}字段, 需人工{m}(搬运{m - nf}/新增{nf})")
  224. if __name__ == "__main__":
  225. main()