|
|
@@ -0,0 +1,607 @@
|
|
|
+"""按最新 DMS 范围重新比对 15 类模板字段,生成补充数据清单。
|
|
|
+
|
|
|
+最新 DMS 范围:
|
|
|
+ 1. 申勤物业数字化/申勤物业项目清单、申勤员工
|
|
|
+ 2. AMS枢元/申勤数字化改造(全部模型)
|
|
|
+
|
|
|
+输出:
|
|
|
+ - data/manual_fill/DMS补数字段清单.csv
|
|
|
+ - output/dms_scoped_template_mapping.csv
|
|
|
+ - docs/DMS字段映射_最新范围.md
|
|
|
+"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import csv
|
|
|
+import json
|
|
|
+import re
|
|
|
+import sys
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+from openpyxl import Workbook, load_workbook
|
|
|
+from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
|
+from openpyxl.utils import get_column_letter
|
|
|
+
|
|
|
+
|
|
|
+sys.stdout.reconfigure(encoding="utf-8")
|
|
|
+
|
|
|
+ROOT = Path(r"E:\CODE\knowledge_agent")
|
|
|
+DMS_JSON = ROOT / "output" / "dms_scoped_models_fields.json"
|
|
|
+OUT_XLSX = ROOT / "data" / "manual_fill" / "DMS补数字段清单.csv"
|
|
|
+OUT_CSV = ROOT / "output" / "dms_scoped_template_mapping.csv"
|
|
|
+OUT_DOC = ROOT / "docs" / "DMS字段映射_最新范围.md"
|
|
|
+
|
|
|
+
|
|
|
+TEMPLATES: dict[str, list[str]] = {
|
|
|
+ "项目信息": ["项目编号", "项目名称", "项目简称", "上级项目编号", "续签前项目编号", "项目起止时间",
|
|
|
+ "省份", "市(区)", "细分业态", "汇总业态", "委托方式", "计费方式", "合同面积",
|
|
|
+ "合同金额", "年化合同额", "合同额具体信息", "服务期限", "合同状况", "首次服务日期",
|
|
|
+ "年化合同收入", "客户满意度", "客户投诉", "甲方名称", "项目地址", "项目负责人",
|
|
|
+ "项目负责人工号", "实际毛利率", "服务状态", "备注"],
|
|
|
+ "项目月度财务": ["项目编号", "项目名称", "年月", "开票金额", "收款金额", "口径说明",
|
|
|
+ "财务经办人工号", "财务经办人", "备注"],
|
|
|
+ "月度快照": ["项目编号", "项目名称", "年月", "在岗人数", "排班人次", "考勤人次",
|
|
|
+ "开票金额", "收款金额", "备注"],
|
|
|
+ "科目余额": ["项目编号", "项目名称", "期间", "科目编码", "科目名称",
|
|
|
+ "借方发生额", "贷方发生额", "期末余额", "备注"],
|
|
|
+ "投标记录": ["项目编号", "项目名称", "年份", "投标类型", "中标结果", "中标日期", "投标金额",
|
|
|
+ "文档路径", "投标经办人工号", "投标经办人", "备注"],
|
|
|
+ "人员信息": ["姓名", "工号", "组织名称", "岗位名称", "当前服务项目编号", "服务起始年月",
|
|
|
+ "服务结束年月", "职级", "员工层级", "所属组织名称", "入职日期", "离职日期", "司龄",
|
|
|
+ "性别", "出生日期", "政治面貌", "联系电话", "备注"],
|
|
|
+ "考勤与人员财务": ["姓名", "工号", "项目编号", "项目名称", "年月", "1-31日", "上班天数",
|
|
|
+ "平时加班小时", "国定加班小时", "餐费补助金额", "加班超时费金额",
|
|
|
+ "国定加班费金额", "值班费金额", "税后工资", "备注"],
|
|
|
+ "排班月报": ["姓名", "工号", "项目编号", "项目名称", "年月", "1-31日班次", "备注"],
|
|
|
+ "岗位编制": ["项目编号", "项目名称", "岗位", "预算编制", "项目编制", "标准工时人数", "在岗人数", "备注"],
|
|
|
+ "设备信息": ["项目编号", "项目名称", "设备编号", "设备类型", "名称", "规格型号参数", "制造厂商",
|
|
|
+ "安装位置", "出厂日期", "启用日期", "入项目日期", "退项目日期", "出厂编号", "完好状况",
|
|
|
+ "原值", "设备责任人工号", "设备责任人", "备注"],
|
|
|
+ "采购与维保": ["记录类型", "项目编号", "项目名称", "供应商名称", "供应商编号", "类别",
|
|
|
+ "名称/合同名称", "数量", "单位", "单价", "金额", "合同年限", "月份/日期",
|
|
|
+ "当前状态", "经办人工号", "经办人", "备注"],
|
|
|
+ "检查记录": ["项目编号", "项目名称", "检查类型", "检查日期", "检查得分", "问题描述",
|
|
|
+ "整改情况", "整改完成日期", "检查人工号", "检查人", "备注"],
|
|
|
+ "片区信息": ["片区编号", "片区名称", "片区负责人工号", "片区负责人", "备注"],
|
|
|
+ "项目片区关系": ["项目编号", "项目名称", "片区编号", "片区名称", "备注"],
|
|
|
+ "人员证书": ["工号", "姓名", "专业类别", "证书类别", "证书名称", "备注"],
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+# 仅使用最新范围内的 DMS 模型。key = (模板, 模板字段)
|
|
|
+DMS_SOURCES: dict[tuple[str, str], str] = {
|
|
|
+ ("项目信息", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("项目信息", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+ ("项目信息", "项目简称"): "shenqin_project_annual_stat.c_project_short_name(项目简称)",
|
|
|
+ ("项目信息", "项目起止时间"): "shenqin_project_annual_stat.c_contract_period(合同起止日期) / sq_project_period.c_c_date_range_text(日期区间)",
|
|
|
+ ("项目信息", "省份"): "ams_sq_project.c_sfzxs(省份/直辖市)",
|
|
|
+ ("项目信息", "市(区)"): "ams_sq_project.c_sq(市/区)",
|
|
|
+ ("项目信息", "细分业态"): "ams_sq_project.c_xfyt(细分业态)",
|
|
|
+ ("项目信息", "汇总业态"): "ams_sq_project.c_hzyt(汇总业态)",
|
|
|
+ ("项目信息", "委托方式"): "ams_sq_project.c_wtfs(委托方式)",
|
|
|
+ ("项目信息", "计费方式"): "ams_sq_project.c_jffs(计费方式)",
|
|
|
+ ("项目信息", "合同面积"): "shenqin_project_annual_stat.c_building_area_sqm(建筑面积) / ams_sq_project.c_htmj(合同面积)",
|
|
|
+ ("项目信息", "合同金额"): "shenqin_project_annual_stat.c_contract_amount(合同金额)",
|
|
|
+ ("项目信息", "年化合同额"): "shenqin_project_annual_stat.c_annualized_amount(年化合同额)",
|
|
|
+ ("项目信息", "合同额具体信息"): "shenqin_project_annual_stat.c_current_period_amount / c_stock_amount / c_new_amount / c_estimated_amount",
|
|
|
+ ("项目信息", "服务期限"): "shenqin_project_annual_stat.c_service_period(服务期限)",
|
|
|
+ ("项目信息", "合同状况"): "shenqin_project_annual_stat.c_contract_status_online / c_contract_status_offline",
|
|
|
+ ("项目信息", "首次服务日期"): "ams_sq_project.c_scfwrq(首次服务日期)",
|
|
|
+ ("项目信息", "年化合同收入"): "ams_sq_project.c_nhhtsr(年化合同收入)",
|
|
|
+ ("项目信息", "甲方名称"): "shenqin_project_annual_stat.c_client_name(甲方名称)",
|
|
|
+ ("项目信息", "项目地址"): "shenqin_project_annual_stat.c_project_address(项目地址) / ams_sq_project.c_xmdz(项目地址)",
|
|
|
+ ("项目信息", "项目负责人"): "ams_sq_project.c_xmdzfzr(项目点状负责人)",
|
|
|
+ ("项目信息", "实际毛利率"): "ams_sq_project.c_xmdnsjmll(项目当年实际毛利率)",
|
|
|
+
|
|
|
+ ("项目月度财务", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("项目月度财务", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("月度快照", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("月度快照", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("科目余额", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("科目余额", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("投标记录", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("投标记录", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+ ("投标记录", "中标结果"): "ams_sq_project.c_zbjgrzbdrhxsj(中标结果)",
|
|
|
+ ("投标记录", "中标日期"): "ams_sq_project.c_xmzbrq(项目中标日期)",
|
|
|
+ ("投标记录", "文档路径"): "sq_project_file.c_c_abs_path(绝对路径) / sq_bid_archive_scan.c_c_abs_path(标书扫描绝对路径)",
|
|
|
+
|
|
|
+ ("人员信息", "姓名"): "sq_employee.c_xm(姓名)",
|
|
|
+ ("人员信息", "工号"): "sq_employee.c_ygbm(员工编码)",
|
|
|
+ ("人员信息", "组织名称"): "sq_employee.c_sszzmc(所属组织名称)",
|
|
|
+ ("人员信息", "岗位名称"): "sq_employee.c_gwmc(岗位名称)",
|
|
|
+ ("人员信息", "服务起始年月"): "sq_employee.c_htksrq(合同开始日期,口径需确认)",
|
|
|
+ ("人员信息", "服务结束年月"): "sq_employee.c_htjsrq(合同结束日期,口径需确认)",
|
|
|
+ ("人员信息", "职级"): "sq_employee.c_zj(职级)",
|
|
|
+ ("人员信息", "员工层级"): "sq_employee.c_ygcj(员工层级)",
|
|
|
+ ("人员信息", "所属组织名称"): "sq_employee.c_sszzmc(所属组织名称)",
|
|
|
+ ("人员信息", "入职日期"): "sq_employee.c_rzrq(入职日期)",
|
|
|
+ ("人员信息", "司龄"): "sq_employee.c_sl(司龄)",
|
|
|
+ ("人员信息", "性别"): "sq_employee.c_xb(性别)",
|
|
|
+ ("人员信息", "出生日期"): "sq_employee.c_csrq(出生日期)",
|
|
|
+ ("人员信息", "政治面貌"): "sq_employee.c_zzmm(政治面貌)",
|
|
|
+ ("人员信息", "联系电话"): "sq_employee.c_sjhm(手机号码)",
|
|
|
+ ("人员信息", "备注"): "sq_employee.c_bz(备注)",
|
|
|
+
|
|
|
+ ("考勤与人员财务", "姓名"): "sq_employee.c_xm(姓名)",
|
|
|
+ ("考勤与人员财务", "工号"): "sq_employee.c_ygbm(员工编码)",
|
|
|
+ ("考勤与人员财务", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("考勤与人员财务", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / sq_employee.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("排班月报", "姓名"): "sq_employee.c_xm(姓名)",
|
|
|
+ ("排班月报", "工号"): "sq_employee.c_ygbm(员工编码)",
|
|
|
+ ("排班月报", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("排班月报", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / sq_employee.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("岗位编制", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("岗位编制", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+ ("岗位编制", "岗位"): "sq_position_salary.c_c_position_name(岗位名称) / sq_employee.c_gwmc(岗位名称)",
|
|
|
+ ("岗位编制", "在岗人数"): "ams_sq_project.c_xmygrs(项目员工人数,需按岗位拆分)",
|
|
|
+
|
|
|
+ ("设备信息", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("设备信息", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("采购与维保", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("采购与维保", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("检查记录", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("检查记录", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+
|
|
|
+ ("片区信息", "片区名称"): "ams_sq_project.c_glqynbkj(管理区域内部口径) / sq_shenqin_project.c_c_region(区域)",
|
|
|
+ ("片区信息", "片区负责人"): "ams_sq_project.c_pjfzr(片区负责人)",
|
|
|
+
|
|
|
+ ("项目片区关系", "项目编号"): "shenqin_project_annual_stat.c_kingdee_code(金蝶编码) / sq_shenqin_project.c_c_project_code(项目编号)",
|
|
|
+ ("项目片区关系", "项目名称"): "shenqin_project_annual_stat.c_project_name(项目名称) / ams_sq_project.c_xmmc(项目名称)",
|
|
|
+ ("项目片区关系", "片区名称"): "ams_sq_project.c_glqynbkj(管理区域内部口径) / sq_shenqin_project.c_c_region(区域)",
|
|
|
+
|
|
|
+ ("人员证书", "工号"): "sq_personnel_certificate.c_c_person_id(人员ID,需与 sq_employee.c_ygbm 核对映射)",
|
|
|
+ ("人员证书", "姓名"): "sq_personnel_material.c_c_name(姓名) / sq_employee.c_xm(姓名)",
|
|
|
+ ("人员证书", "证书类别"): "sq_personnel_certificate.c_c_cert_type(证书类型)",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+# 与 DMS/其他补充表对齐的主连接键。即使 DMS 已有,也必须在补充 Excel 中保留。
|
|
|
+JOIN_KEYS: dict[str, set[str]] = {
|
|
|
+ "项目信息": {"项目编号"},
|
|
|
+ "项目月度财务": {"项目编号", "年月"},
|
|
|
+ "月度快照": {"项目编号", "年月"},
|
|
|
+ "科目余额": {"项目编号", "期间", "科目编码"},
|
|
|
+ "投标记录": {"项目编号", "年份"},
|
|
|
+ "人员信息": {"工号"},
|
|
|
+ "考勤与人员财务": {"工号", "项目编号", "年月"},
|
|
|
+ "排班月报": {"工号", "项目编号", "年月"},
|
|
|
+ "岗位编制": {"项目编号", "岗位"},
|
|
|
+ "设备信息": {"项目编号", "设备编号"},
|
|
|
+ "采购与维保": {"项目编号", "名称/合同名称", "月份/日期"},
|
|
|
+ "检查记录": {"项目编号", "检查日期"},
|
|
|
+ "片区信息": {"片区编号"},
|
|
|
+ "项目片区关系": {"项目编号", "片区编号"},
|
|
|
+ "人员证书": {"工号", "证书名称"},
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+# 非 DMS 字段中,可从已有源 Excel 搬运的字段
|
|
|
+SOURCE_EXCEL_FIELDS: set[tuple[str, str]] = {
|
|
|
+ ("项目信息", "上级项目编号"),
|
|
|
+ ("项目信息", "续签前项目编号"),
|
|
|
+ ("项目信息", "客户满意度"),
|
|
|
+ ("项目信息", "客户投诉"),
|
|
|
+ ("项目月度财务", "年月"),
|
|
|
+ ("项目月度财务", "开票金额"),
|
|
|
+ ("项目月度财务", "收款金额"),
|
|
|
+ ("月度快照", "年月"),
|
|
|
+ ("月度快照", "在岗人数"),
|
|
|
+ ("月度快照", "排班人次"),
|
|
|
+ ("月度快照", "考勤人次"),
|
|
|
+ ("月度快照", "开票金额"),
|
|
|
+ ("月度快照", "收款金额"),
|
|
|
+ ("科目余额", "期间"),
|
|
|
+ ("科目余额", "科目编码"),
|
|
|
+ ("科目余额", "科目名称"),
|
|
|
+ ("科目余额", "借方发生额"),
|
|
|
+ ("科目余额", "贷方发生额"),
|
|
|
+ ("科目余额", "期末余额"),
|
|
|
+ ("投标记录", "年份"),
|
|
|
+ ("投标记录", "投标金额"),
|
|
|
+ ("人员信息", "离职日期"),
|
|
|
+ ("考勤与人员财务", "年月"),
|
|
|
+ ("考勤与人员财务", "1-31日"),
|
|
|
+ ("考勤与人员财务", "上班天数"),
|
|
|
+ ("考勤与人员财务", "平时加班小时"),
|
|
|
+ ("考勤与人员财务", "国定加班小时"),
|
|
|
+ ("考勤与人员财务", "餐费补助金额"),
|
|
|
+ ("考勤与人员财务", "加班超时费金额"),
|
|
|
+ ("考勤与人员财务", "国定加班费金额"),
|
|
|
+ ("考勤与人员财务", "值班费金额"),
|
|
|
+ ("考勤与人员财务", "税后工资"),
|
|
|
+ ("考勤与人员财务", "备注"),
|
|
|
+ ("排班月报", "年月"),
|
|
|
+ ("排班月报", "1-31日班次"),
|
|
|
+ ("岗位编制", "预算编制"),
|
|
|
+ ("岗位编制", "项目编制"),
|
|
|
+ ("岗位编制", "标准工时人数"),
|
|
|
+ ("设备信息", "设备编号"),
|
|
|
+ ("设备信息", "设备类型"),
|
|
|
+ ("设备信息", "名称"),
|
|
|
+ ("设备信息", "规格型号参数"),
|
|
|
+ ("设备信息", "制造厂商"),
|
|
|
+ ("设备信息", "安装位置"),
|
|
|
+ ("设备信息", "出厂日期"),
|
|
|
+ ("设备信息", "启用日期"),
|
|
|
+ ("设备信息", "出厂编号"),
|
|
|
+ ("设备信息", "完好状况"),
|
|
|
+ ("设备信息", "原值"),
|
|
|
+ ("采购与维保", "供应商名称"),
|
|
|
+ ("采购与维保", "类别"),
|
|
|
+ ("采购与维保", "名称/合同名称"),
|
|
|
+ ("采购与维保", "数量"),
|
|
|
+ ("采购与维保", "单位"),
|
|
|
+ ("采购与维保", "单价"),
|
|
|
+ ("采购与维保", "金额"),
|
|
|
+ ("采购与维保", "合同年限"),
|
|
|
+ ("采购与维保", "月份/日期"),
|
|
|
+ ("采购与维保", "当前状态"),
|
|
|
+ ("检查记录", "检查类型"),
|
|
|
+ ("检查记录", "检查日期"),
|
|
|
+ ("检查记录", "检查得分"),
|
|
|
+ ("检查记录", "问题描述"),
|
|
|
+ ("检查记录", "整改情况"),
|
|
|
+ ("检查记录", "整改完成日期"),
|
|
|
+ ("人员证书", "专业类别"),
|
|
|
+ ("人员证书", "证书名称"),
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+DEFAULT_SOURCES: dict[str, str] = {
|
|
|
+ "项目信息": "市场部项目管理表 / 业务大表",
|
|
|
+ "项目月度财务": "财务部收费进程表(1-12月物业费/期内开票收款)",
|
|
|
+ "月度快照": "由考勤/排班/项目月度财务汇总生成(可参考 scripts/sample_snapshot.py)",
|
|
|
+ "科目余额": "财务部科目余额表(科目编码/名称/借贷发生额/期末余额)",
|
|
|
+ "投标记录": "业务大表中标情况 / 档案目录",
|
|
|
+ "人员信息": "人事部员工花名册",
|
|
|
+ "考勤与人员财务": "人事部考勤表(13份考勤 xls/xlsx)",
|
|
|
+ "排班月报": "人事部排班文件(每日班次代码)",
|
|
|
+ "岗位编制": "运营部 9-项目岗位编制汇总.xlsx",
|
|
|
+ "设备信息": "运营部设备台账(青浦工业园区/徐汇环境监测中心)",
|
|
|
+ "采购与维保": "采购部耗材汇总/维保合同台账/固定资产台账",
|
|
|
+ "检查记录": "运营部品质巡检及整改记录台账",
|
|
|
+ "片区信息": "业务大表「管理区域」/ 8-项目信息汇总表",
|
|
|
+ "项目片区关系": "业务大表「管理区域」推导",
|
|
|
+ "人员证书": "合景悠活职称及资格证书管理登记表",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+SPECIAL_NOTES: dict[tuple[str, str], str] = {
|
|
|
+ ("项目信息", "项目负责人工号"): "DMS 有项目负责人姓名;工号需按姓名在员工花名册中匹配",
|
|
|
+ ("项目信息", "上级项目编号"): "可由业务大表层级/委托方式推导后回填",
|
|
|
+ ("项目信息", "续签前项目编号"): "可由续签链/档案目录推导后回填",
|
|
|
+ ("人员信息", "当前服务项目编号"): "DMS sq_employee 只有项目名称/GUID,编号需按项目名称映射",
|
|
|
+ ("人员信息", "离职日期"): "DMS 只有合同结束日期,不是离职日期,需人工补",
|
|
|
+ ("月度快照", "在岗人数"): "优先按考勤明细不同工号数统计,并与岗位编制在岗人数核对",
|
|
|
+ ("月度快照", "排班人次"): "排班月报按项目+年月汇总行数",
|
|
|
+ ("月度快照", "考勤人次"): "考勤明细按项目+年月汇总行数",
|
|
|
+ ("科目余额", "科目编码"): "财务科目编码,同一项目同一期间内唯一",
|
|
|
+ ("科目余额", "科目名称"): "与科目编码配套的中文科目名称",
|
|
|
+ ("考勤与人员财务", "税后工资"): "源考勤表无该列名,部分文件金额填在表头为「员工签字」的列",
|
|
|
+ ("考勤与人员财务", "1-31日"): "值为 常/日/夜/日1 等班次标记,原样搬运",
|
|
|
+ ("排班月报", "1-31日班次"): "值为班次代码,如 OFF/A003/SQ-104/M11-033;OFF=休息",
|
|
|
+ ("设备信息", "设备责任人工号"): "模板必填;源台账可能有责任人姓名,工号需匹配花名册",
|
|
|
+ ("采购与维保", "供应商编号"): "用于关联供应商主数据;DMS 范围内无供应商模型,需新编或另建",
|
|
|
+ ("片区信息", "片区编号"): "最新 DMS 范围内没有 xzbm 片区模型,需人工统一编码",
|
|
|
+ ("项目片区关系", "片区编号"): "同上,需与片区信息表保持一致",
|
|
|
+ ("人员证书", "证书名称"): "DMS 证书表只有证书类型/证书号掩码,证书全称需补充",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+def norm_header(h: str, tpl: str) -> str:
|
|
|
+ h = str(h or "").strip()
|
|
|
+ h = re.sub(r"\*$", "", h)
|
|
|
+ h = re.sub(r"([^)]*)$", "", h)
|
|
|
+ if re.fullmatch(r"\d{1,2}日", h):
|
|
|
+ return "1-31日班次" if tpl == "排班月报" else "1-31日"
|
|
|
+ return h
|
|
|
+
|
|
|
+
|
|
|
+def read_required() -> dict[str, set[str]]:
|
|
|
+ required: dict[str, set[str]] = {}
|
|
|
+ for f in sorted((ROOT / "data" / "templates").glob("*.csv")):
|
|
|
+ tpl = f.stem
|
|
|
+ req: set[str] = set()
|
|
|
+ with f.open("r", encoding="utf-8-sig", newline="") as fh:
|
|
|
+ reader = csv.reader(fh)
|
|
|
+ header = next(reader, [])
|
|
|
+ for h in header:
|
|
|
+ if h and "*" in str(h):
|
|
|
+ req.add(norm_header(str(h), tpl))
|
|
|
+ required[tpl] = req
|
|
|
+ return required
|
|
|
+
|
|
|
+
|
|
|
+def build_rows() -> list[dict]:
|
|
|
+ required = read_required()
|
|
|
+ rows: list[dict] = []
|
|
|
+ for tpl, fields in TEMPLATES.items():
|
|
|
+ req = required.get(tpl, set())
|
|
|
+ for i, fld in enumerate(fields, start=1):
|
|
|
+ has_dms = (tpl, fld) in DMS_SOURCES
|
|
|
+ is_join = fld in JOIN_KEYS.get(tpl, set())
|
|
|
+ include = (not has_dms) or is_join
|
|
|
+
|
|
|
+ if has_dms and not is_join:
|
|
|
+ kind = "DMS已有"
|
|
|
+ elif has_dms and is_join:
|
|
|
+ kind = "连接字段(DMS已有,Excel需带)"
|
|
|
+ elif is_join:
|
|
|
+ kind = "连接字段(需补充)"
|
|
|
+ elif (tpl, fld) in SOURCE_EXCEL_FIELDS:
|
|
|
+ kind = "业务字段(源Excel补充)"
|
|
|
+ else:
|
|
|
+ kind = "业务字段(新增/人工确认)"
|
|
|
+
|
|
|
+ note = SPECIAL_NOTES.get((tpl, fld), "")
|
|
|
+ if not note and not has_dms and (tpl, fld) in SOURCE_EXCEL_FIELDS:
|
|
|
+ note = DEFAULT_SOURCES.get(tpl, "")
|
|
|
+ elif not note and not has_dms:
|
|
|
+ note = "DMS 范围内无对应字段,需人工确认"
|
|
|
+ elif not note and has_dms:
|
|
|
+ note = DMS_SOURCES[(tpl, fld)]
|
|
|
+
|
|
|
+ rows.append({
|
|
|
+ "template": tpl,
|
|
|
+ "seq": i,
|
|
|
+ "field": fld,
|
|
|
+ "has_dms": has_dms,
|
|
|
+ "dms_source": DMS_SOURCES.get((tpl, fld), ""),
|
|
|
+ "kind": kind,
|
|
|
+ "is_join": is_join,
|
|
|
+ "include": include,
|
|
|
+ "required": "是" if fld in req else "否",
|
|
|
+ "note": note,
|
|
|
+ })
|
|
|
+ return rows
|
|
|
+
|
|
|
+
|
|
|
+def write_xlsx(rows: list[dict]) -> None:
|
|
|
+ wb = Workbook()
|
|
|
+ header_fill = PatternFill("solid", fgColor="2F5597")
|
|
|
+ header_font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
|
|
|
+ body_font = Font(name="微软雅黑", size=10)
|
|
|
+ thin = Side(style="thin", color="BFBFBF")
|
|
|
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
|
|
+
|
|
|
+ # Sheet 1:全量字段比对
|
|
|
+ ws = wb.active
|
|
|
+ ws.title = "字段比对总表"
|
|
|
+ headers = ["模板", "序号", "模板字段", "DMS是否有", "DMS来源(模型.字段)", "补充类型",
|
|
|
+ "是否连接字段", "是否需放入补充Excel", "模板必填", "数据来源/填报说明"]
|
|
|
+ widths = [16, 6, 18, 9, 46, 24, 11, 16, 8, 52]
|
|
|
+ for c, (h, w) in enumerate(zip(headers, widths), start=1):
|
|
|
+ cell = ws.cell(row=1, column=c, value=h)
|
|
|
+ cell.font = header_font
|
|
|
+ cell.fill = header_fill
|
|
|
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
|
|
+ cell.border = border
|
|
|
+ ws.column_dimensions[get_column_letter(c)].width = w
|
|
|
+ ws.row_dimensions[1].height = 28
|
|
|
+
|
|
|
+ r = 2
|
|
|
+ for row in rows:
|
|
|
+ vals = [
|
|
|
+ row["template"], row["seq"], row["field"],
|
|
|
+ "是" if row["has_dms"] else "否",
|
|
|
+ row["dms_source"], row["kind"],
|
|
|
+ "是" if row["is_join"] else "否",
|
|
|
+ "是" if row["include"] else "否",
|
|
|
+ row["required"], row["note"],
|
|
|
+ ]
|
|
|
+ for c, v in enumerate(vals, start=1):
|
|
|
+ cell = ws.cell(row=r, column=c, value=v)
|
|
|
+ cell.font = body_font
|
|
|
+ cell.border = border
|
|
|
+ cell.alignment = Alignment(vertical="top", wrap_text=True)
|
|
|
+ if row["kind"].startswith("连接字段"):
|
|
|
+ ws.cell(row=r, column=6).fill = PatternFill("solid", fgColor="FFF2CC")
|
|
|
+ elif row["has_dms"]:
|
|
|
+ ws.cell(row=r, column=6).fill = PatternFill("solid", fgColor="EDEDED")
|
|
|
+ else:
|
|
|
+ ws.cell(row=r, column=6).fill = PatternFill("solid", fgColor="FCE4D6")
|
|
|
+ if row["required"] == "是":
|
|
|
+ ws.cell(row=r, column=9).fill = PatternFill("solid", fgColor="FCE4D6")
|
|
|
+ r += 1
|
|
|
+ ws.freeze_panes = "A2"
|
|
|
+ ws.auto_filter.ref = f"A1:J{r - 1}"
|
|
|
+
|
|
|
+ # Sheet 2:待补充字段清单
|
|
|
+ ws2 = wb.create_sheet("待补充字段清单")
|
|
|
+ for c, (h, w) in enumerate(zip(headers, widths), start=1):
|
|
|
+ cell = ws2.cell(row=1, column=c, value=h)
|
|
|
+ cell.font = header_font
|
|
|
+ cell.fill = header_fill
|
|
|
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
|
|
+ cell.border = border
|
|
|
+ ws2.column_dimensions[get_column_letter(c)].width = w
|
|
|
+ ws2.row_dimensions[1].height = 28
|
|
|
+ r2 = 2
|
|
|
+ for row in rows:
|
|
|
+ if not row["include"]:
|
|
|
+ continue
|
|
|
+ vals = [
|
|
|
+ row["template"], row["seq"], row["field"],
|
|
|
+ "是" if row["has_dms"] else "否",
|
|
|
+ row["dms_source"], row["kind"],
|
|
|
+ "是" if row["is_join"] else "否",
|
|
|
+ "是", row["required"], row["note"],
|
|
|
+ ]
|
|
|
+ for c, v in enumerate(vals, start=1):
|
|
|
+ cell = ws2.cell(row=r2, column=c, value=v)
|
|
|
+ cell.font = body_font
|
|
|
+ cell.border = border
|
|
|
+ cell.alignment = Alignment(vertical="top", wrap_text=True)
|
|
|
+ if row["kind"].startswith("连接字段"):
|
|
|
+ ws2.cell(row=r2, column=6).fill = PatternFill("solid", fgColor="FFF2CC")
|
|
|
+ else:
|
|
|
+ ws2.cell(row=r2, column=6).fill = PatternFill("solid", fgColor="FCE4D6")
|
|
|
+ r2 += 1
|
|
|
+ ws2.freeze_panes = "A2"
|
|
|
+ ws2.auto_filter.ref = f"A1:J{r2 - 1}"
|
|
|
+
|
|
|
+ # Sheet 3:连接字段清单
|
|
|
+ ws3 = wb.create_sheet("连接字段清单")
|
|
|
+ h3 = ["模板", "连接字段", "DMS是否有", "DMS来源", "连接说明"]
|
|
|
+ w3 = [16, 20, 9, 46, 46]
|
|
|
+ for c, (h, w) in enumerate(zip(h3, w3), start=1):
|
|
|
+ cell = ws3.cell(row=1, column=c, value=h)
|
|
|
+ cell.font = header_font
|
|
|
+ cell.fill = header_fill
|
|
|
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
|
|
+ cell.border = border
|
|
|
+ ws3.column_dimensions[get_column_letter(c)].width = w
|
|
|
+ ws3.row_dimensions[1].height = 24
|
|
|
+ r3 = 2
|
|
|
+ for tpl, keys in JOIN_KEYS.items():
|
|
|
+ for k in sorted(keys):
|
|
|
+ has_dms = (tpl, k) in DMS_SOURCES
|
|
|
+ src = DMS_SOURCES.get((tpl, k), "")
|
|
|
+ note = f"{tpl} 补充表与 DMS 数据按该字段对齐"
|
|
|
+ vals = [tpl, k, "是" if has_dms else "否", src, note]
|
|
|
+ for c, v in enumerate(vals, start=1):
|
|
|
+ cell = ws3.cell(row=r3, column=c, value=v)
|
|
|
+ cell.font = body_font
|
|
|
+ cell.border = border
|
|
|
+ cell.alignment = Alignment(vertical="top", wrap_text=True)
|
|
|
+ if not has_dms:
|
|
|
+ ws3.cell(row=r3, column=3).fill = PatternFill("solid", fgColor="FCE4D6")
|
|
|
+ r3 += 1
|
|
|
+ ws3.freeze_panes = "A2"
|
|
|
+ ws3.auto_filter.ref = f"A1:E{r3 - 1}"
|
|
|
+
|
|
|
+ # Sheet 4:统计汇总
|
|
|
+ ws4 = wb.create_sheet("统计汇总")
|
|
|
+ ws4.column_dimensions["A"].width = 34
|
|
|
+ ws4.column_dimensions["B"].width = 12
|
|
|
+ ws4.column_dimensions["C"].width = 52
|
|
|
+ total = len(rows)
|
|
|
+ dms_count = sum(1 for x in rows if x["has_dms"])
|
|
|
+ missing_count = total - dms_count
|
|
|
+ source_excel_count = sum(1 for x in rows if (not x["has_dms"]) and (x["template"], x["field"]) in SOURCE_EXCEL_FIELDS)
|
|
|
+ new_count = missing_count - source_excel_count
|
|
|
+ join_count = sum(1 for x in rows if x["is_join"])
|
|
|
+ join_dms = sum(1 for x in rows if x["is_join"] and x["has_dms"])
|
|
|
+ join_missing = join_count - join_dms
|
|
|
+ supplement_count = missing_count + join_dms
|
|
|
+
|
|
|
+ summary = [
|
|
|
+ ("统计项", "数值", "说明"),
|
|
|
+ ("最新范围 DMS 模型数", 24, "申勤物业项目清单/申勤员工 2 个 + AMS枢元/申勤数字化改造 22 个"),
|
|
|
+ ("15 类模板字段总数", total, "1-31日 按 1 个字段统计(实际是 31 列)"),
|
|
|
+ ("DMS 已有字段数", dms_count, "可从本次范围内 DMS 模型自动获取"),
|
|
|
+ ("DMS 无字段数(需业务补充)", missing_count, "DMS 范围内无对应字段"),
|
|
|
+ ("其中:源Excel可搬运", source_excel_count, "已有源 Excel,但 DMS 未建模型,需搬运"),
|
|
|
+ ("其中:需新增/人工确认", new_count, "DMS 与源 Excel 均无,需人工确认/新建"),
|
|
|
+ ("连接字段数", join_count, "用于补充 Excel 与 DMS/其他表对齐"),
|
|
|
+ ("其中:DMS已有但Excel需带", join_dms, "如项目编号、工号;补充表也必须保留"),
|
|
|
+ ("其中:连接字段需补充", join_missing, "如年月、设备编号、片区编号等"),
|
|
|
+ ("补充 Excel 需包含字段总数", supplement_count, "= DMS 无字段数 + DMS 已有的连接字段数"),
|
|
|
+ ]
|
|
|
+ for r, row in enumerate(summary, start=1):
|
|
|
+ for c, v in enumerate(row, start=1):
|
|
|
+ cell = ws4.cell(row=r, column=c, value=v)
|
|
|
+ cell.border = border
|
|
|
+ cell.alignment = Alignment(vertical="top", wrap_text=True)
|
|
|
+ if r == 1:
|
|
|
+ cell.font = header_font
|
|
|
+ cell.fill = header_fill
|
|
|
+ else:
|
|
|
+ cell.font = body_font
|
|
|
+ if c == 1:
|
|
|
+ cell.font = Font(name="微软雅黑", size=10, bold=True)
|
|
|
+ ws4.freeze_panes = "A2"
|
|
|
+
|
|
|
+ wb.save(OUT_XLSX)
|
|
|
+
|
|
|
+
|
|
|
+def write_csv(rows: list[dict]) -> None:
|
|
|
+ with OUT_CSV.open("w", newline="", encoding="utf-8-sig") as f:
|
|
|
+ w = csv.writer(f)
|
|
|
+ w.writerow(["模板", "序号", "模板字段", "DMS是否有", "DMS来源(模型.字段)",
|
|
|
+ "补充类型", "是否连接字段", "是否需放入补充Excel", "模板必填", "数据来源/填报说明"])
|
|
|
+ for row in rows:
|
|
|
+ w.writerow([
|
|
|
+ row["template"], row["seq"], row["field"],
|
|
|
+ "是" if row["has_dms"] else "否", row["dms_source"],
|
|
|
+ row["kind"], "是" if row["is_join"] else "否",
|
|
|
+ "是" if row["include"] else "否", row["required"], row["note"],
|
|
|
+ ])
|
|
|
+
|
|
|
+
|
|
|
+def write_markdown(rows: list[dict]) -> None:
|
|
|
+ total = len(rows)
|
|
|
+ dms_count = sum(1 for x in rows if x["has_dms"])
|
|
|
+ missing_count = total - dms_count
|
|
|
+ source_excel_count = sum(1 for x in rows if (not x["has_dms"]) and (x["template"], x["field"]) in SOURCE_EXCEL_FIELDS)
|
|
|
+ new_count = missing_count - source_excel_count
|
|
|
+ join_count = sum(1 for x in rows if x["is_join"])
|
|
|
+ join_dms = sum(1 for x in rows if x["is_join"] and x["has_dms"])
|
|
|
+ join_missing = join_count - join_dms
|
|
|
+ supplement_count = missing_count + join_dms
|
|
|
+
|
|
|
+ lines = [
|
|
|
+ "# DMS 数据库字段映射(最新范围)",
|
|
|
+ "",
|
|
|
+ "> 依据本次指定的 DMS 数据范围重新生成:",
|
|
|
+ "> 1. 申勤物业数字化:申勤物业项目清单、申勤员工",
|
|
|
+ "> 2. AMS枢元 -- 申勤数字化改造(全部模型)",
|
|
|
+ "",
|
|
|
+ "## 总览",
|
|
|
+ "",
|
|
|
+ f"- 范围内 DMS 模型数:24",
|
|
|
+ f"- 15 类模板字段总数:{total}",
|
|
|
+ f"- DMS 已有字段:{dms_count}",
|
|
|
+ f"- DMS 无字段(需业务补充):{missing_count}(源Excel可搬运 {source_excel_count} / 需新增或人工确认 {new_count})",
|
|
|
+ f"- 连接字段:{join_count}(DMS已有但 Excel 需带 {join_dms} / 需补充 {join_missing})",
|
|
|
+ f"- 补充 Excel 需包含字段总数:{supplement_count}",
|
|
|
+ "",
|
|
|
+ "## 覆盖概览",
|
|
|
+ "",
|
|
|
+ "| 模板 | 字段数 | DMS已有 | DMS无 | 需放入补充Excel |",
|
|
|
+ "|---|---:|---:|---:|---:|",
|
|
|
+ ]
|
|
|
+ for tpl, fields in TEMPLATES.items():
|
|
|
+ trows = [x for x in rows if x["template"] == tpl]
|
|
|
+ has = sum(1 for x in trows if x["has_dms"])
|
|
|
+ inc = sum(1 for x in trows if x["include"])
|
|
|
+ lines.append(f"| {tpl} | {len(fields)} | {has} | {len(fields) - has} | {inc} |")
|
|
|
+
|
|
|
+ lines += ["", "## 字段级映射", ""]
|
|
|
+ for tpl, fields in TEMPLATES.items():
|
|
|
+ lines.append(f"### {tpl}")
|
|
|
+ lines.append("| 模板字段 | DMS是否有 | DMS来源 | 补充类型 | 连接字段 | 说明 |")
|
|
|
+ lines.append("|---|---|---|---|---|---|")
|
|
|
+ for x in rows:
|
|
|
+ if x["template"] != tpl:
|
|
|
+ continue
|
|
|
+ lines.append(
|
|
|
+ f"| {x['field']} | {'是' if x['has_dms'] else '否'} | "
|
|
|
+ f"{x['dms_source'] or '—'} | {x['kind']} | "
|
|
|
+ f"{'是' if x['is_join'] else '否'} | {x['note']} |"
|
|
|
+ )
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ OUT_DOC.write_text("\n".join(lines), encoding="utf-8")
|
|
|
+
|
|
|
+
|
|
|
+def main() -> None:
|
|
|
+ rows = build_rows()
|
|
|
+ OUT_XLSX.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+ write_csv(rows)
|
|
|
+ OUT_XLSX.write_bytes(OUT_CSV.read_bytes())
|
|
|
+ write_markdown(rows)
|
|
|
+ print("输出:", OUT_XLSX)
|
|
|
+ print("输出:", OUT_CSV)
|
|
|
+ print("输出:", OUT_DOC)
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|