fix: 智能导入——匹配不上的财务报表科目自动创建KPI定义
- 清理科目前缀(一、/减:/加:)后多级匹配 - ⑤仍未匹配→自动创建KPI(PL_001/CF_001/BS_001) - 避免205条全部跳过的场景
This commit is contained in:
+47
-13
@@ -190,10 +190,16 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
|
|||||||
for code in known_codes:
|
for code in known_codes:
|
||||||
clean = re.sub(r'[\s\-_()()]', '', code).lower()
|
clean = re.sub(r'[\s\-_()()]', '', code).lower()
|
||||||
alias_map[clean] = code
|
alias_map[clean] = code
|
||||||
|
# 中文名映射("营业收入"→F_REVENUE)
|
||||||
|
name_map: dict[str, str] = {}
|
||||||
|
for code, kpi_obj in kpis.items():
|
||||||
|
name_map[kpi_obj.kpi_name] = code
|
||||||
|
|
||||||
# 8. 遍历导入
|
# 8. 遍历导入(匹配不上的自动创建KPI)
|
||||||
|
stype_prefix = {"PL": "PL_", "CF": "CF_", "BS": "BS_"}.get(stype or "", "EXT_")
|
||||||
batch = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:12]
|
batch = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:12]
|
||||||
imported = 0
|
imported = 0
|
||||||
|
created_kpis = 0
|
||||||
skipped_rows = []
|
skipped_rows = []
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
for idx, row in df.iterrows():
|
||||||
@@ -208,24 +214,50 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
|
|||||||
skipped_rows.append(f"第{idx+2}行: 无法确定期间")
|
skipped_rows.append(f"第{idx+2}行: 无法确定期间")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 智能匹配KPI编码
|
# 清理科目名(去掉"一、""减:""加:"等前缀)
|
||||||
|
clean_name = re.sub(r'^[一二三四五六七八九十、\s\+]+', '', raw_kpi)
|
||||||
|
clean_name = re.sub(r'^[减加]?[::]\s*', '', clean_name).strip()
|
||||||
|
if not clean_name:
|
||||||
|
clean_name = raw_kpi
|
||||||
|
|
||||||
|
# 匹配KPI
|
||||||
kpi_code = None
|
kpi_code = None
|
||||||
|
|
||||||
|
# ① 精确编码匹配(极少情况)
|
||||||
if raw_kpi in known_codes:
|
if raw_kpi in known_codes:
|
||||||
kpi_code = raw_kpi
|
kpi_code = raw_kpi
|
||||||
else:
|
# ② 别名匹配(去符号小写)
|
||||||
# 别名匹配
|
if not kpi_code:
|
||||||
clean_key = re.sub(r'[\s\-_()()]', '', raw_kpi).lower()
|
clean_key = re.sub(r'[\s\-_()()]', '', raw_kpi).lower()
|
||||||
kpi_code = alias_map.get(clean_key)
|
kpi_code = alias_map.get(clean_key)
|
||||||
# 模糊匹配(中文科目名→KPI编码)
|
# ③ 中文名精确匹配
|
||||||
if not kpi_code:
|
|
||||||
for code, kpi_obj in kpis.items():
|
|
||||||
if raw_kpi in kpi_obj.kpi_name or kpi_obj.kpi_name in raw_kpi:
|
|
||||||
kpi_code = code
|
|
||||||
break
|
|
||||||
|
|
||||||
if not kpi_code:
|
if not kpi_code:
|
||||||
skipped_rows.append(f"第{idx+2}行: 「{raw_kpi}」未匹配到KPI")
|
kpi_code = name_map.get(clean_name)
|
||||||
continue
|
# ④ 中文名模糊匹配
|
||||||
|
if not kpi_code:
|
||||||
|
for code, kpi_obj in kpis.items():
|
||||||
|
if clean_name in kpi_obj.kpi_name or kpi_obj.kpi_name in clean_name:
|
||||||
|
kpi_code = code
|
||||||
|
break
|
||||||
|
|
||||||
|
# ⑤ 仍未匹配 → 自动创建KPI
|
||||||
|
if not kpi_code:
|
||||||
|
new_code = f"{stype_prefix}{len(kpis) + created_kpis + 1:03d}"
|
||||||
|
new_kpi = KPIDefinition(
|
||||||
|
kpi_code=new_code,
|
||||||
|
kpi_name=clean_name,
|
||||||
|
dimension="finance",
|
||||||
|
category="financial_report",
|
||||||
|
data_source_type="excel",
|
||||||
|
status="active",
|
||||||
|
)
|
||||||
|
db.add(new_kpi)
|
||||||
|
db.flush()
|
||||||
|
kpis[new_code] = new_kpi
|
||||||
|
known_codes.add(new_code)
|
||||||
|
name_map[clean_name] = new_code
|
||||||
|
kpi_code = new_code
|
||||||
|
created_kpis += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
val = float(raw_val)
|
val = float(raw_val)
|
||||||
@@ -248,6 +280,8 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
|
|||||||
# 9. 返回汇总
|
# 9. 返回汇总
|
||||||
stype_label = {"PL": "利润表", "CF": "现金流量表", "BS": "资产负债表"}.get(stype or "", "数据表")
|
stype_label = {"PL": "利润表", "CF": "现金流量表", "BS": "资产负债表"}.get(stype or "", "数据表")
|
||||||
msg = f"✅ {stype_label}识别成功,导入{imported}条"
|
msg = f"✅ {stype_label}识别成功,导入{imported}条"
|
||||||
|
if created_kpis:
|
||||||
|
msg += f",自动创建{created_kpis}个新KPI"
|
||||||
if skipped_rows:
|
if skipped_rows:
|
||||||
msg += f",{len(skipped_rows)}条跳过:\n" + "\n".join(skipped_rows[:8])
|
msg += f",{len(skipped_rows)}条跳过:\n" + "\n".join(skipped_rows[:8])
|
||||||
if len(skipped_rows) > 8:
|
if len(skipped_rows) > 8:
|
||||||
|
|||||||
@@ -233,6 +233,77 @@ def get_kpi_score(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# KPI-glossary: 知识资产化 — KPI字典实时加载(供ChatBI财务Bot调用)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@router.get("/glossary")
|
||||||
|
def get_kpi_glossary(
|
||||||
|
entity_id: int = Query(1, ge=1),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user = Depends(require_auth),
|
||||||
|
):
|
||||||
|
"""KPI字典实时加载 — 返回所有KPI的定义、当前值、目标值、公式、维度、阈值
|
||||||
|
|
||||||
|
供ChatBI财务Bot在分析前调用,确保口径与系统一致。
|
||||||
|
返回字段: kpi_code, kpi_name, current_value, target_value, formula, dimension, threshold
|
||||||
|
"""
|
||||||
|
kpis = db.query(KPIDefinition).filter(
|
||||||
|
KPIDefinition.status == "active",
|
||||||
|
KPIDefinition.entity_id == entity_id,
|
||||||
|
).order_by(KPIDefinition.kpi_code).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for k in kpis:
|
||||||
|
# 获取最新实际值
|
||||||
|
latest_val = db.query(KPIValue).filter(
|
||||||
|
KPIValue.kpi_id == k.id,
|
||||||
|
KPIValue.actual_value.isnot(None),
|
||||||
|
).order_by(KPIValue.period.desc()).first()
|
||||||
|
|
||||||
|
current_value = latest_val.actual_value if latest_val else None
|
||||||
|
latest_period = latest_val.period if latest_val else None
|
||||||
|
|
||||||
|
# 组装阈值描述
|
||||||
|
threshold = None
|
||||||
|
if k.threshold_green or k.threshold_yellow or k.threshold_red:
|
||||||
|
parts = []
|
||||||
|
if k.threshold_green:
|
||||||
|
parts.append(f"绿灯:{k.threshold_green}")
|
||||||
|
if k.threshold_yellow:
|
||||||
|
parts.append(f"黄灯:{k.threshold_yellow}")
|
||||||
|
if k.threshold_red:
|
||||||
|
parts.append(f"红灯:{k.threshold_red}")
|
||||||
|
threshold = " | ".join(parts)
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"kpi_id": k.id,
|
||||||
|
"kpi_code": k.kpi_code,
|
||||||
|
"kpi_name": k.kpi_name,
|
||||||
|
"dimension": k.dimension,
|
||||||
|
"category": k.category,
|
||||||
|
"formula": k.formula,
|
||||||
|
"formula_desc": k.formula_desc,
|
||||||
|
"unit": k.unit,
|
||||||
|
"target_value": k.target_value,
|
||||||
|
"current_value": current_value,
|
||||||
|
"latest_period": latest_period,
|
||||||
|
"threshold": threshold,
|
||||||
|
"responsible_dept": k.responsible_dept,
|
||||||
|
"responsible_user": k.responsible_user,
|
||||||
|
"data_source": k.data_source,
|
||||||
|
"data_owner": k.data_owner,
|
||||||
|
"frequency": k.frequency,
|
||||||
|
"status": k.status,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
"total": len(result),
|
||||||
|
"glossary": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# KPI-6: KPI三级分解树
|
# KPI-6: KPI三级分解树
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user