generate_dms_supplement.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. """按最新 DMS 范围重新比对 15 类模板字段,生成补充数据清单。
  2. 最新 DMS 范围:
  3. 1. 申勤物业数字化/申勤物业项目清单、申勤员工
  4. 2. AMS枢元/申勤数字化改造(全部模型)
  5. 输出:
  6. - data/manual_fill/DMS补数字段清单.csv
  7. - output/dms_scoped_template_mapping.csv
  8. - docs/DMS字段映射_最新范围.md
  9. """
  10. from __future__ import annotations
  11. import csv
  12. import json
  13. import re
  14. import sys
  15. from pathlib import Path
  16. from openpyxl import Workbook, load_workbook
  17. from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
  18. from openpyxl.utils import get_column_letter
  19. sys.stdout.reconfigure(encoding="utf-8")
  20. ROOT = Path(r"E:\CODE\knowledge_agent")
  21. DMS_JSON = ROOT / "output" / "dms_scoped_models_fields.json"
  22. OUT_XLSX = ROOT / "data" / "manual_fill" / "DMS补数字段清单.csv"
  23. OUT_CSV = ROOT / "output" / "dms_scoped_template_mapping.csv"
  24. OUT_DOC = ROOT / "docs" / "DMS字段映射_最新范围.md"
  25. TEMPLATES: dict[str, list[str]] = {
  26. "项目信息": ["项目编号", "项目名称", "项目简称", "上级项目编号", "续签前项目编号", "项目起止时间",
  27. "省份", "市(区)", "细分业态", "汇总业态", "委托方式", "计费方式", "合同面积",
  28. "合同金额", "年化合同额", "合同额具体信息", "服务期限", "合同状况", "首次服务日期",
  29. "年化合同收入", "客户满意度", "客户投诉", "甲方名称", "项目地址", "项目负责人",
  30. "项目负责人工号", "实际毛利率", "服务状态", "备注"],
  31. "项目月度财务": ["项目编号", "项目名称", "年月", "开票金额", "收款金额", "口径说明",
  32. "财务经办人工号", "财务经办人", "备注"],
  33. "月度快照": ["项目编号", "项目名称", "年月", "在岗人数", "排班人次", "考勤人次",
  34. "开票金额", "收款金额", "备注"],
  35. "科目余额": ["项目编号", "项目名称", "期间", "科目编码", "科目名称",
  36. "借方发生额", "贷方发生额", "期末余额", "备注"],
  37. "投标记录": ["项目编号", "项目名称", "年份", "投标类型", "中标结果", "中标日期", "投标金额",
  38. "文档路径", "投标经办人工号", "投标经办人", "备注"],
  39. "人员信息": ["姓名", "工号", "组织名称", "岗位名称", "当前服务项目编号", "服务起始年月",
  40. "服务结束年月", "职级", "员工层级", "所属组织名称", "入职日期", "离职日期", "司龄",
  41. "性别", "出生日期", "政治面貌", "联系电话", "备注"],
  42. "考勤与人员财务": ["姓名", "工号", "项目编号", "项目名称", "年月", "1-31日", "上班天数",
  43. "平时加班小时", "国定加班小时", "餐费补助金额", "加班超时费金额",
  44. "国定加班费金额", "值班费金额", "税后工资", "备注"],
  45. "排班月报": ["姓名", "工号", "项目编号", "项目名称", "年月", "1-31日班次", "备注"],
  46. "岗位编制": ["项目编号", "项目名称", "岗位", "预算编制", "项目编制", "标准工时人数", "在岗人数", "备注"],
  47. "设备信息": ["项目编号", "项目名称", "设备编号", "设备类型", "名称", "规格型号参数", "制造厂商",
  48. "安装位置", "出厂日期", "启用日期", "入项目日期", "退项目日期", "出厂编号", "完好状况",
  49. "原值", "设备责任人工号", "设备责任人", "备注"],
  50. "采购与维保": ["记录类型", "项目编号", "项目名称", "供应商名称", "供应商编号", "类别",
  51. "名称/合同名称", "数量", "单位", "单价", "金额", "合同年限", "月份/日期",
  52. "当前状态", "经办人工号", "经办人", "备注"],
  53. "检查记录": ["项目编号", "项目名称", "检查类型", "检查日期", "检查得分", "问题描述",
  54. "整改情况", "整改完成日期", "检查人工号", "检查人", "备注"],
  55. "片区信息": ["片区编号", "片区名称", "片区负责人工号", "片区负责人", "备注"],
  56. "项目片区关系": ["项目编号", "项目名称", "片区编号", "片区名称", "备注"],
  57. "人员证书": ["工号", "姓名", "专业类别", "证书类别", "证书名称", "备注"],
  58. }
  59. # 仅使用最新范围内的 DMS 模型。key = (模板, 模板字段)
  60. DMS_SOURCES: dict[tuple[str, str], str] = {
  61. ("项目信息", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  62. ("项目信息", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  63. ("项目信息", "项目简称"): "shenqin_project_annual_stat.c_project_short_name(项目简称)",
  64. ("项目信息", "项目起止时间"): "shenqin_project_annual_stat.c_contract_period(合同起止日期) / sq_project_period.c_c_date_range_text(日期区间)",
  65. ("项目信息", "省份"): "ams_sq_project.c_sfzxs(省份/直辖市)",
  66. ("项目信息", "市(区)"): "ams_sq_project.c_sq(市/区)",
  67. ("项目信息", "细分业态"): "ams_sq_project.c_xfyt(细分业态)",
  68. ("项目信息", "汇总业态"): "ams_sq_project.c_hzyt(汇总业态)",
  69. ("项目信息", "委托方式"): "ams_sq_project.c_wtfs(委托方式)",
  70. ("项目信息", "计费方式"): "ams_sq_project.c_jffs(计费方式)",
  71. ("项目信息", "合同面积"): "shenqin_project_annual_stat.c_building_area_sqm(建筑面积) / ams_sq_project.c_htmj(合同面积)",
  72. ("项目信息", "合同金额"): "shenqin_project_annual_stat.c_contract_amount(合同金额)",
  73. ("项目信息", "年化合同额"): "shenqin_project_annual_stat.c_annualized_amount(年化合同额)",
  74. ("项目信息", "合同额具体信息"): "shenqin_project_annual_stat.c_current_period_amount / c_stock_amount / c_new_amount / c_estimated_amount",
  75. ("项目信息", "服务期限"): "shenqin_project_annual_stat.c_service_period(服务期限)",
  76. ("项目信息", "合同状况"): "shenqin_project_annual_stat.c_contract_status_online / c_contract_status_offline",
  77. ("项目信息", "首次服务日期"): "ams_sq_project.c_scfwrq(首次服务日期)",
  78. ("项目信息", "年化合同收入"): "ams_sq_project.c_nhhtsr(年化合同收入)",
  79. ("项目信息", "甲方名称"): "shenqin_project_annual_stat.c_client_name(甲方名称)",
  80. ("项目信息", "项目地址"): "shenqin_project_annual_stat.c_project_address(项目地址) / ams_sq_project.c_xmdz(项目地址)",
  81. ("项目信息", "项目负责人"): "ams_sq_project.c_xmdzfzr(项目点状负责人)",
  82. ("项目信息", "实际毛利率"): "ams_sq_project.c_xmdnsjmll(项目当年实际毛利率)",
  83. ("项目月度财务", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  84. ("项目月度财务", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  85. ("月度快照", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  86. ("月度快照", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  87. ("科目余额", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  88. ("科目余额", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  89. ("投标记录", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  90. ("投标记录", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  91. ("投标记录", "中标结果"): "ams_sq_project.c_zbjgrzbdrhxsj(中标结果)",
  92. ("投标记录", "中标日期"): "ams_sq_project.c_xmzbrq(项目中标日期)",
  93. ("投标记录", "文档路径"): "sq_project_file.c_c_abs_path(绝对路径) / sq_bid_archive_scan.c_c_abs_path(标书扫描绝对路径)",
  94. ("人员信息", "姓名"): "sq_employee.c_xm(姓名)",
  95. ("人员信息", "工号"): "sq_employee.c_ygbm(员工编码)",
  96. ("人员信息", "组织名称"): "sq_employee.c_sszzmc(所属组织名称)",
  97. ("人员信息", "岗位名称"): "sq_employee.c_gwmc(岗位名称)",
  98. ("人员信息", "服务起始年月"): "sq_employee.c_htksrq(合同开始日期,口径需确认)",
  99. ("人员信息", "服务结束年月"): "sq_employee.c_htjsrq(合同结束日期,口径需确认)",
  100. ("人员信息", "职级"): "sq_employee.c_zj(职级)",
  101. ("人员信息", "员工层级"): "sq_employee.c_ygcj(员工层级)",
  102. ("人员信息", "所属组织名称"): "sq_employee.c_sszzmc(所属组织名称)",
  103. ("人员信息", "入职日期"): "sq_employee.c_rzrq(入职日期)",
  104. ("人员信息", "司龄"): "sq_employee.c_sl(司龄)",
  105. ("人员信息", "性别"): "sq_employee.c_xb(性别)",
  106. ("人员信息", "出生日期"): "sq_employee.c_csrq(出生日期)",
  107. ("人员信息", "政治面貌"): "sq_employee.c_zzmm(政治面貌)",
  108. ("人员信息", "联系电话"): "sq_employee.c_sjhm(手机号码)",
  109. ("人员信息", "备注"): "sq_employee.c_bz(备注)",
  110. ("考勤与人员财务", "姓名"): "sq_employee.c_xm(姓名)",
  111. ("考勤与人员财务", "工号"): "sq_employee.c_ygbm(员工编码)",
  112. ("考勤与人员财务", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  113. ("考勤与人员财务", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / sq_employee.c_xmmc(项目名称)",
  114. ("排班月报", "姓名"): "sq_employee.c_xm(姓名)",
  115. ("排班月报", "工号"): "sq_employee.c_ygbm(员工编码)",
  116. ("排班月报", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  117. ("排班月报", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / sq_employee.c_xmmc(项目名称)",
  118. ("岗位编制", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  119. ("岗位编制", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  120. ("岗位编制", "岗位"): "sq_position_salary.c_c_position_name(岗位名称) / sq_employee.c_gwmc(岗位名称)",
  121. ("岗位编制", "在岗人数"): "ams_sq_project.c_xmygrs(项目员工人数,需按岗位拆分)",
  122. ("设备信息", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  123. ("设备信息", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  124. ("采购与维保", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  125. ("采购与维保", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  126. ("检查记录", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  127. ("检查记录", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  128. ("片区信息", "片区名称"): "ams_sq_project.c_glqynbkj(管理区域内部口径) / sq_shenqin_project.c_c_region(区域)",
  129. ("片区信息", "片区负责人"): "ams_sq_project.c_pjfzr(片区负责人)",
  130. ("项目片区关系", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
  131. ("项目片区关系", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
  132. ("项目片区关系", "片区名称"): "ams_sq_project.c_glqynbkj(管理区域内部口径) / sq_shenqin_project.c_c_region(区域)",
  133. ("人员证书", "工号"): "sq_personnel_certificate.c_c_person_id(人员ID,需与 sq_employee.c_ygbm 核对映射)",
  134. ("人员证书", "姓名"): "sq_personnel_material.c_c_name(姓名) / sq_employee.c_xm(姓名)",
  135. ("人员证书", "证书类别"): "sq_personnel_certificate.c_c_cert_type(证书类型)",
  136. }
  137. # 与 DMS/其他补充表对齐的主连接键。即使 DMS 已有,也必须在补充 Excel 中保留。
  138. JOIN_KEYS: dict[str, set[str]] = {
  139. "项目信息": {"项目编号"},
  140. "项目月度财务": {"项目编号", "年月"},
  141. "月度快照": {"项目编号", "年月"},
  142. "科目余额": {"项目编号", "期间", "科目编码"},
  143. "投标记录": {"项目编号", "年份"},
  144. "人员信息": {"工号"},
  145. "考勤与人员财务": {"工号", "项目编号", "年月"},
  146. "排班月报": {"工号", "项目编号", "年月"},
  147. "岗位编制": {"项目编号", "岗位"},
  148. "设备信息": {"项目编号", "设备编号"},
  149. "采购与维保": {"项目编号", "名称/合同名称", "月份/日期"},
  150. "检查记录": {"项目编号", "检查日期"},
  151. "片区信息": {"片区编号"},
  152. "项目片区关系": {"项目编号", "片区编号"},
  153. "人员证书": {"工号", "证书名称"},
  154. }
  155. # 非 DMS 字段中,可从已有源 Excel 搬运的字段
  156. SOURCE_EXCEL_FIELDS: set[tuple[str, str]] = {
  157. ("项目信息", "上级项目编号"),
  158. ("项目信息", "续签前项目编号"),
  159. ("项目信息", "客户满意度"),
  160. ("项目信息", "客户投诉"),
  161. ("项目月度财务", "年月"),
  162. ("项目月度财务", "开票金额"),
  163. ("项目月度财务", "收款金额"),
  164. ("月度快照", "年月"),
  165. ("月度快照", "在岗人数"),
  166. ("月度快照", "排班人次"),
  167. ("月度快照", "考勤人次"),
  168. ("月度快照", "开票金额"),
  169. ("月度快照", "收款金额"),
  170. ("科目余额", "期间"),
  171. ("科目余额", "科目编码"),
  172. ("科目余额", "科目名称"),
  173. ("科目余额", "借方发生额"),
  174. ("科目余额", "贷方发生额"),
  175. ("科目余额", "期末余额"),
  176. ("投标记录", "年份"),
  177. ("投标记录", "投标金额"),
  178. ("人员信息", "离职日期"),
  179. ("考勤与人员财务", "年月"),
  180. ("考勤与人员财务", "1-31日"),
  181. ("考勤与人员财务", "上班天数"),
  182. ("考勤与人员财务", "平时加班小时"),
  183. ("考勤与人员财务", "国定加班小时"),
  184. ("考勤与人员财务", "餐费补助金额"),
  185. ("考勤与人员财务", "加班超时费金额"),
  186. ("考勤与人员财务", "国定加班费金额"),
  187. ("考勤与人员财务", "值班费金额"),
  188. ("考勤与人员财务", "税后工资"),
  189. ("考勤与人员财务", "备注"),
  190. ("排班月报", "年月"),
  191. ("排班月报", "1-31日班次"),
  192. ("岗位编制", "预算编制"),
  193. ("岗位编制", "项目编制"),
  194. ("岗位编制", "标准工时人数"),
  195. ("设备信息", "设备编号"),
  196. ("设备信息", "设备类型"),
  197. ("设备信息", "名称"),
  198. ("设备信息", "规格型号参数"),
  199. ("设备信息", "制造厂商"),
  200. ("设备信息", "安装位置"),
  201. ("设备信息", "出厂日期"),
  202. ("设备信息", "启用日期"),
  203. ("设备信息", "出厂编号"),
  204. ("设备信息", "完好状况"),
  205. ("设备信息", "原值"),
  206. ("采购与维保", "供应商名称"),
  207. ("采购与维保", "类别"),
  208. ("采购与维保", "名称/合同名称"),
  209. ("采购与维保", "数量"),
  210. ("采购与维保", "单位"),
  211. ("采购与维保", "单价"),
  212. ("采购与维保", "金额"),
  213. ("采购与维保", "合同年限"),
  214. ("采购与维保", "月份/日期"),
  215. ("采购与维保", "当前状态"),
  216. ("检查记录", "检查类型"),
  217. ("检查记录", "检查日期"),
  218. ("检查记录", "检查得分"),
  219. ("检查记录", "问题描述"),
  220. ("检查记录", "整改情况"),
  221. ("检查记录", "整改完成日期"),
  222. ("人员证书", "专业类别"),
  223. ("人员证书", "证书名称"),
  224. }
  225. DEFAULT_SOURCES: dict[str, str] = {
  226. "项目信息": "市场部项目管理表 / 业务大表",
  227. "项目月度财务": "财务部收费进程表(1-12月物业费/期内开票收款)",
  228. "月度快照": "由考勤/排班/项目月度财务汇总生成(可参考 scripts/sample_snapshot.py)",
  229. "科目余额": "财务部科目余额表(科目编码/名称/借贷发生额/期末余额)",
  230. "投标记录": "业务大表中标情况 / 档案目录",
  231. "人员信息": "人事部员工花名册",
  232. "考勤与人员财务": "人事部考勤表(13份考勤 xls/xlsx)",
  233. "排班月报": "人事部排班文件(每日班次代码)",
  234. "岗位编制": "运营部 9-项目岗位编制汇总.xlsx",
  235. "设备信息": "运营部设备台账(青浦工业园区/徐汇环境监测中心)",
  236. "采购与维保": "采购部耗材汇总/维保合同台账/固定资产台账",
  237. "检查记录": "运营部品质巡检及整改记录台账",
  238. "片区信息": "业务大表「管理区域」/ 8-项目信息汇总表",
  239. "项目片区关系": "业务大表「管理区域」推导",
  240. "人员证书": "合景悠活职称及资格证书管理登记表",
  241. }
  242. SPECIAL_NOTES: dict[tuple[str, str], str] = {
  243. ("项目信息", "项目负责人工号"): "DMS 有项目负责人姓名;工号需按姓名在员工花名册中匹配",
  244. ("项目信息", "上级项目编号"): "可由业务大表层级/委托方式推导后回填",
  245. ("项目信息", "续签前项目编号"): "可由续签链/档案目录推导后回填",
  246. ("人员信息", "当前服务项目编号"): "DMS sq_employee 只有项目名称/GUID,编号需按项目名称映射",
  247. ("人员信息", "离职日期"): "DMS 只有合同结束日期,不是离职日期,需人工补",
  248. ("月度快照", "在岗人数"): "优先按考勤明细不同工号数统计,并与岗位编制在岗人数核对",
  249. ("月度快照", "排班人次"): "排班月报按项目+年月汇总行数",
  250. ("月度快照", "考勤人次"): "考勤明细按项目+年月汇总行数",
  251. ("科目余额", "科目编码"): "财务科目编码,同一项目同一期间内唯一",
  252. ("科目余额", "科目名称"): "与科目编码配套的中文科目名称",
  253. ("考勤与人员财务", "税后工资"): "源考勤表无该列名,部分文件金额填在表头为「员工签字」的列",
  254. ("考勤与人员财务", "1-31日"): "值为 常/日/夜/日1 等班次标记,原样搬运",
  255. ("排班月报", "1-31日班次"): "值为班次代码,如 OFF/A003/SQ-104/M11-033;OFF=休息",
  256. ("设备信息", "设备责任人工号"): "模板必填;源台账可能有责任人姓名,工号需匹配花名册",
  257. ("采购与维保", "供应商编号"): "用于关联供应商主数据;DMS 范围内无供应商模型,需新编或另建",
  258. ("片区信息", "片区编号"): "最新 DMS 范围内没有 xzbm 片区模型,需人工统一编码",
  259. ("项目片区关系", "片区编号"): "同上,需与片区信息表保持一致",
  260. ("人员证书", "证书名称"): "DMS 证书表只有证书类型/证书号掩码,证书全称需补充",
  261. }
  262. def norm_header(h: str, tpl: str) -> str:
  263. h = str(h or "").strip()
  264. h = re.sub(r"\*$", "", h)
  265. h = re.sub(r"([^)]*)$", "", h)
  266. if re.fullmatch(r"\d{1,2}日", h):
  267. return "1-31日班次" if tpl == "排班月报" else "1-31日"
  268. return h
  269. def read_required() -> dict[str, set[str]]:
  270. required: dict[str, set[str]] = {}
  271. for f in sorted((ROOT / "data" / "templates").glob("*.csv")):
  272. tpl = f.stem
  273. req: set[str] = set()
  274. with f.open("r", encoding="utf-8-sig", newline="") as fh:
  275. reader = csv.reader(fh)
  276. header = next(reader, [])
  277. for h in header:
  278. if h and "*" in str(h):
  279. req.add(norm_header(str(h), tpl))
  280. required[tpl] = req
  281. return required
  282. def build_rows() -> list[dict]:
  283. required = read_required()
  284. rows: list[dict] = []
  285. for tpl, fields in TEMPLATES.items():
  286. req = required.get(tpl, set())
  287. for i, fld in enumerate(fields, start=1):
  288. has_dms = (tpl, fld) in DMS_SOURCES
  289. is_join = fld in JOIN_KEYS.get(tpl, set())
  290. include = (not has_dms) or is_join
  291. if has_dms and not is_join:
  292. kind = "DMS已有"
  293. elif has_dms and is_join:
  294. kind = "连接字段(DMS已有,Excel需带)"
  295. elif is_join:
  296. kind = "连接字段(需补充)"
  297. elif (tpl, fld) in SOURCE_EXCEL_FIELDS:
  298. kind = "业务字段(源Excel补充)"
  299. else:
  300. kind = "业务字段(新增/人工确认)"
  301. note = SPECIAL_NOTES.get((tpl, fld), "")
  302. if not note and not has_dms and (tpl, fld) in SOURCE_EXCEL_FIELDS:
  303. note = DEFAULT_SOURCES.get(tpl, "")
  304. elif not note and not has_dms:
  305. note = "DMS 范围内无对应字段,需人工确认"
  306. elif not note and has_dms:
  307. note = DMS_SOURCES[(tpl, fld)]
  308. rows.append({
  309. "template": tpl,
  310. "seq": i,
  311. "field": fld,
  312. "has_dms": has_dms,
  313. "dms_source": DMS_SOURCES.get((tpl, fld), ""),
  314. "kind": kind,
  315. "is_join": is_join,
  316. "include": include,
  317. "required": "是" if fld in req else "否",
  318. "note": note,
  319. })
  320. return rows
  321. def write_xlsx(rows: list[dict]) -> None:
  322. wb = Workbook()
  323. header_fill = PatternFill("solid", fgColor="2F5597")
  324. header_font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
  325. body_font = Font(name="微软雅黑", size=10)
  326. thin = Side(style="thin", color="BFBFBF")
  327. border = Border(left=thin, right=thin, top=thin, bottom=thin)
  328. # Sheet 1:全量字段比对
  329. ws = wb.active
  330. ws.title = "字段比对总表"
  331. headers = ["模板", "序号", "模板字段", "DMS是否有", "DMS来源(模型.字段)", "补充类型",
  332. "是否连接字段", "是否需放入补充Excel", "模板必填", "数据来源/填报说明"]
  333. widths = [16, 6, 18, 9, 46, 24, 11, 16, 8, 52]
  334. for c, (h, w) in enumerate(zip(headers, widths), start=1):
  335. cell = ws.cell(row=1, column=c, value=h)
  336. cell.font = header_font
  337. cell.fill = header_fill
  338. cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
  339. cell.border = border
  340. ws.column_dimensions[get_column_letter(c)].width = w
  341. ws.row_dimensions[1].height = 28
  342. r = 2
  343. for row in rows:
  344. vals = [
  345. row["template"], row["seq"], row["field"],
  346. "是" if row["has_dms"] else "否",
  347. row["dms_source"], row["kind"],
  348. "是" if row["is_join"] else "否",
  349. "是" if row["include"] else "否",
  350. row["required"], row["note"],
  351. ]
  352. for c, v in enumerate(vals, start=1):
  353. cell = ws.cell(row=r, column=c, value=v)
  354. cell.font = body_font
  355. cell.border = border
  356. cell.alignment = Alignment(vertical="top", wrap_text=True)
  357. if row["kind"].startswith("连接字段"):
  358. ws.cell(row=r, column=6).fill = PatternFill("solid", fgColor="FFF2CC")
  359. elif row["has_dms"]:
  360. ws.cell(row=r, column=6).fill = PatternFill("solid", fgColor="EDEDED")
  361. else:
  362. ws.cell(row=r, column=6).fill = PatternFill("solid", fgColor="FCE4D6")
  363. if row["required"] == "是":
  364. ws.cell(row=r, column=9).fill = PatternFill("solid", fgColor="FCE4D6")
  365. r += 1
  366. ws.freeze_panes = "A2"
  367. ws.auto_filter.ref = f"A1:J{r - 1}"
  368. # Sheet 2:待补充字段清单
  369. ws2 = wb.create_sheet("待补充字段清单")
  370. for c, (h, w) in enumerate(zip(headers, widths), start=1):
  371. cell = ws2.cell(row=1, column=c, value=h)
  372. cell.font = header_font
  373. cell.fill = header_fill
  374. cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
  375. cell.border = border
  376. ws2.column_dimensions[get_column_letter(c)].width = w
  377. ws2.row_dimensions[1].height = 28
  378. r2 = 2
  379. for row in rows:
  380. if not row["include"]:
  381. continue
  382. vals = [
  383. row["template"], row["seq"], row["field"],
  384. "是" if row["has_dms"] else "否",
  385. row["dms_source"], row["kind"],
  386. "是" if row["is_join"] else "否",
  387. "是", row["required"], row["note"],
  388. ]
  389. for c, v in enumerate(vals, start=1):
  390. cell = ws2.cell(row=r2, column=c, value=v)
  391. cell.font = body_font
  392. cell.border = border
  393. cell.alignment = Alignment(vertical="top", wrap_text=True)
  394. if row["kind"].startswith("连接字段"):
  395. ws2.cell(row=r2, column=6).fill = PatternFill("solid", fgColor="FFF2CC")
  396. else:
  397. ws2.cell(row=r2, column=6).fill = PatternFill("solid", fgColor="FCE4D6")
  398. r2 += 1
  399. ws2.freeze_panes = "A2"
  400. ws2.auto_filter.ref = f"A1:J{r2 - 1}"
  401. # Sheet 3:连接字段清单
  402. ws3 = wb.create_sheet("连接字段清单")
  403. h3 = ["模板", "连接字段", "DMS是否有", "DMS来源", "连接说明"]
  404. w3 = [16, 20, 9, 46, 46]
  405. for c, (h, w) in enumerate(zip(h3, w3), start=1):
  406. cell = ws3.cell(row=1, column=c, value=h)
  407. cell.font = header_font
  408. cell.fill = header_fill
  409. cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
  410. cell.border = border
  411. ws3.column_dimensions[get_column_letter(c)].width = w
  412. ws3.row_dimensions[1].height = 24
  413. r3 = 2
  414. for tpl, keys in JOIN_KEYS.items():
  415. for k in sorted(keys):
  416. has_dms = (tpl, k) in DMS_SOURCES
  417. src = DMS_SOURCES.get((tpl, k), "")
  418. note = f"{tpl} 补充表与 DMS 数据按该字段对齐"
  419. vals = [tpl, k, "是" if has_dms else "否", src, note]
  420. for c, v in enumerate(vals, start=1):
  421. cell = ws3.cell(row=r3, column=c, value=v)
  422. cell.font = body_font
  423. cell.border = border
  424. cell.alignment = Alignment(vertical="top", wrap_text=True)
  425. if not has_dms:
  426. ws3.cell(row=r3, column=3).fill = PatternFill("solid", fgColor="FCE4D6")
  427. r3 += 1
  428. ws3.freeze_panes = "A2"
  429. ws3.auto_filter.ref = f"A1:E{r3 - 1}"
  430. # Sheet 4:统计汇总
  431. ws4 = wb.create_sheet("统计汇总")
  432. ws4.column_dimensions["A"].width = 34
  433. ws4.column_dimensions["B"].width = 12
  434. ws4.column_dimensions["C"].width = 52
  435. total = len(rows)
  436. dms_count = sum(1 for x in rows if x["has_dms"])
  437. missing_count = total - dms_count
  438. source_excel_count = sum(1 for x in rows if (not x["has_dms"]) and (x["template"], x["field"]) in SOURCE_EXCEL_FIELDS)
  439. new_count = missing_count - source_excel_count
  440. join_count = sum(1 for x in rows if x["is_join"])
  441. join_dms = sum(1 for x in rows if x["is_join"] and x["has_dms"])
  442. join_missing = join_count - join_dms
  443. supplement_count = missing_count + join_dms
  444. summary = [
  445. ("统计项", "数值", "说明"),
  446. ("最新范围 DMS 模型数", 24, "申勤物业项目清单/申勤员工 2 个 + AMS枢元/申勤数字化改造 22 个"),
  447. ("15 类模板字段总数", total, "1-31日 按 1 个字段统计(实际是 31 列)"),
  448. ("DMS 已有字段数", dms_count, "可从本次范围内 DMS 模型自动获取"),
  449. ("DMS 无字段数(需业务补充)", missing_count, "DMS 范围内无对应字段"),
  450. ("其中:源Excel可搬运", source_excel_count, "已有源 Excel,但 DMS 未建模型,需搬运"),
  451. ("其中:需新增/人工确认", new_count, "DMS 与源 Excel 均无,需人工确认/新建"),
  452. ("连接字段数", join_count, "用于补充 Excel 与 DMS/其他表对齐"),
  453. ("其中:DMS已有但Excel需带", join_dms, "如项目编号、工号;补充表也必须保留"),
  454. ("其中:连接字段需补充", join_missing, "如年月、设备编号、片区编号等"),
  455. ("补充 Excel 需包含字段总数", supplement_count, "= DMS 无字段数 + DMS 已有的连接字段数"),
  456. ]
  457. for r, row in enumerate(summary, start=1):
  458. for c, v in enumerate(row, start=1):
  459. cell = ws4.cell(row=r, column=c, value=v)
  460. cell.border = border
  461. cell.alignment = Alignment(vertical="top", wrap_text=True)
  462. if r == 1:
  463. cell.font = header_font
  464. cell.fill = header_fill
  465. else:
  466. cell.font = body_font
  467. if c == 1:
  468. cell.font = Font(name="微软雅黑", size=10, bold=True)
  469. ws4.freeze_panes = "A2"
  470. wb.save(OUT_XLSX)
  471. def write_csv(rows: list[dict]) -> None:
  472. with OUT_CSV.open("w", newline="", encoding="utf-8-sig") as f:
  473. w = csv.writer(f)
  474. w.writerow(["模板", "序号", "模板字段", "DMS是否有", "DMS来源(模型.字段)",
  475. "补充类型", "是否连接字段", "是否需放入补充Excel", "模板必填", "数据来源/填报说明"])
  476. for row in rows:
  477. w.writerow([
  478. row["template"], row["seq"], row["field"],
  479. "是" if row["has_dms"] else "否", row["dms_source"],
  480. row["kind"], "是" if row["is_join"] else "否",
  481. "是" if row["include"] else "否", row["required"], row["note"],
  482. ])
  483. def write_markdown(rows: list[dict]) -> None:
  484. total = len(rows)
  485. dms_count = sum(1 for x in rows if x["has_dms"])
  486. missing_count = total - dms_count
  487. source_excel_count = sum(1 for x in rows if (not x["has_dms"]) and (x["template"], x["field"]) in SOURCE_EXCEL_FIELDS)
  488. new_count = missing_count - source_excel_count
  489. join_count = sum(1 for x in rows if x["is_join"])
  490. join_dms = sum(1 for x in rows if x["is_join"] and x["has_dms"])
  491. join_missing = join_count - join_dms
  492. supplement_count = missing_count + join_dms
  493. lines = [
  494. "# DMS 数据库字段映射(最新范围)",
  495. "",
  496. "> 依据本次指定的 DMS 数据范围重新生成:",
  497. "> 1. 申勤物业数字化:申勤物业项目清单、申勤员工",
  498. "> 2. AMS枢元 -- 申勤数字化改造(全部模型)",
  499. "",
  500. "## 总览",
  501. "",
  502. f"- 范围内 DMS 模型数:24",
  503. f"- 15 类模板字段总数:{total}",
  504. f"- DMS 已有字段:{dms_count}",
  505. f"- DMS 无字段(需业务补充):{missing_count}(源Excel可搬运 {source_excel_count} / 需新增或人工确认 {new_count})",
  506. f"- 连接字段:{join_count}(DMS已有但 Excel 需带 {join_dms} / 需补充 {join_missing})",
  507. f"- 补充 Excel 需包含字段总数:{supplement_count}",
  508. "",
  509. "## 覆盖概览",
  510. "",
  511. "| 模板 | 字段数 | DMS已有 | DMS无 | 需放入补充Excel |",
  512. "|---|---:|---:|---:|---:|",
  513. ]
  514. for tpl, fields in TEMPLATES.items():
  515. trows = [x for x in rows if x["template"] == tpl]
  516. has = sum(1 for x in trows if x["has_dms"])
  517. inc = sum(1 for x in trows if x["include"])
  518. lines.append(f"| {tpl} | {len(fields)} | {has} | {len(fields) - has} | {inc} |")
  519. lines += ["", "## 字段级映射", ""]
  520. for tpl, fields in TEMPLATES.items():
  521. lines.append(f"### {tpl}")
  522. lines.append("| 模板字段 | DMS是否有 | DMS来源 | 补充类型 | 连接字段 | 说明 |")
  523. lines.append("|---|---|---|---|---|---|")
  524. for x in rows:
  525. if x["template"] != tpl:
  526. continue
  527. lines.append(
  528. f"| {x['field']} | {'是' if x['has_dms'] else '否'} | "
  529. f"{x['dms_source'] or '—'} | {x['kind']} | "
  530. f"{'是' if x['is_join'] else '否'} | {x['note']} |"
  531. )
  532. lines.append("")
  533. OUT_DOC.write_text("\n".join(lines), encoding="utf-8")
  534. def main() -> None:
  535. rows = build_rows()
  536. OUT_XLSX.parent.mkdir(parents=True, exist_ok=True)
  537. write_csv(rows)
  538. OUT_XLSX.write_bytes(OUT_CSV.read_bytes())
  539. write_markdown(rows)
  540. print("输出:", OUT_XLSX)
  541. print("输出:", OUT_CSV)
  542. print("输出:", OUT_DOC)
  543. if __name__ == "__main__":
  544. main()