fix(fetch-caliber): 取数层排除半年/预测伪期间 + subjects 响应补 entity_id

R3 期间口径(仅 YYYY-MM)延伸到取数层:新增 alert_entity_scope.month_period_clause()(跨库可移植 SQL 等价式),应用到 15 个文件 26 处「按 period 倒序取最新值/最近N期」位点;dashboard 原 MySQL-only REGEXP 正则收口到同单一事实源。

GET /api/cma/subjects 响应增补 entity_id(NULL=全局标准准则科目 / N=账套私有明细),既有 10 字段语义与名称不变。

证据:pending-tasks/_reports/yanxue-20260924-cma-fetch-caliber-fixes-fullstack.md(改前/改后 A/B:F_NET_PROFIT 2026-H2/-55.0 → 2026-08/-10.46)。新增 7 例回归测试;全量 1 failed(预存在) 1473 passed,基线同项。
This commit is contained in:
Hermes CI Fix
2026-09-26 02:42:04 +08:00
parent 52b693ddbb
commit 242f3cf5ec
17 changed files with 282 additions and 18 deletions
+8 -3
View File
@@ -11,6 +11,7 @@ from app.models import KPIDefinition, KPIValue, KPIAlert, User, VoucherDetail
from app.utils.cache import get as cache_get, set as cache_set
from app.utils.voucher_guard import apply_voucher_guard
from app.utils.threshold_parser import eval_threshold
from app.utils.alert_entity_scope import month_period_clause
# 指标方向(反向/正向)唯一权威源 = app.api.kpis.REVERSE_INDICATORS(2026-09-15 M2 准入2 收口)。
# 本文件原自带一份字面量副本,与 kpis.py 漂移后会与「能力评分」判定相反,故改为导入。
from app.api.kpis import REVERSE_INDICATORS
@@ -113,6 +114,7 @@ def get_bep_card(
KPIValue.kpi_id == kpis[code].id,
KPIValue.source_type == "derived",
KPIValue.actual_value.isnot(None),
month_period_clause(KPIValue.period),
).order_by(KPIValue.period.desc(), KPIValue.id.desc()).first()
latest[code] = {"period": v.period, "value": v.actual_value} if v else None
@@ -333,7 +335,7 @@ def get_dashboard_kpis(role: str = Query("ceo"), period: str = Query("month"),
latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None
latest = type('obj', (object,), {"actual_value": latest_val, "period": f"{start_date}~{end_date}"})() if latest_val else None
else:
latest = base_query.order_by(KPIValue.period.desc()).first()
latest = base_query.filter(month_period_clause(KPIValue.period)).order_by(KPIValue.period.desc()).first()
alert = db.query(KPIAlert).filter(
KPIAlert.kpi_id == k.id,
@@ -396,6 +398,7 @@ def get_my_kpis(
trend_values = db.query(KPIValue).filter(
KPIValue.kpi_id == k.id,
month_period_clause(KPIValue.period),
).order_by(KPIValue.period.desc()).limit(6).all()
trend = [{"period": v.period, "value": v.actual_value} for v in reversed(trend_values)]
@@ -446,6 +449,7 @@ def get_finance_analysis(
trend_values = db.query(KPIValue).filter(
KPIValue.kpi_id == k.id,
month_period_clause(KPIValue.period),
).order_by(KPIValue.period.desc()).limit(6).all()
trend = [{"period": v.period, "value": v.actual_value} for v in reversed(trend_values)]
@@ -578,7 +582,8 @@ def predict_kpis(db: Session = Depends(get_db), entity_id: int = Depends(get_ent
# H1/H2/FY 等汇总期行,则回退到原按 calculated_at 的取法(不改变无月期 KPI 的行为)。
# 注意:**不能**只按 period 字典序排 —— '2026-H1'/'2026-H2' 字典序大于 '2026-08',会把
# 季度汇总行/半年度预估值(forecast)当成"最新"。
_MONTH_PERIOD_REGEX = r"^[0-9]{4}-(0[1-9]|1[0-2])$"
# 期间口径(R3 正则 / SQL 等价式)已于 2026-09-26 收口到 app/utils/alert_entity_scope.py
# (is_month_period / month_period_clause),此处不再保留第二份副本。
# 主值优先(2026-09-18 同期间重复行判据收口,项目单 project-20260918-kpi-row-priority)
# 同一期间可能并存两行:①主值(source_type='ledger' / data_status='verified',结账/复核口径)
@@ -600,7 +605,7 @@ def latest_kpi_value(db: Session, kpi_id: int) -> Optional[KPIValue]:
"""该 KPI 的最新可用值:月期间优先(同期内主值优先),无月期间行时回退 calculated_at 倒序。"""
monthly = (
db.query(KPIValue)
.filter(KPIValue.kpi_id == kpi_id, KPIValue.period.op("REGEXP")(_MONTH_PERIOD_REGEX))
.filter(KPIValue.kpi_id == kpi_id, month_period_clause(KPIValue.period))
.order_by(
KPIValue.period.desc(),
_MAIN_VALUE_PRIORITY,