# -*- coding: utf-8 -*- """KPI 取值分区口径 —— 「实测区 / 预估区」单一事实源(财务裁定 2026-10-02) 依据:`wecom-finance/pending-tasks/finance-20261002-h2-forecast-actual-separation-fullstack.md` (任总 2026-10-02 11:35 CST DM 直令「把 H2 预测行与实际分离」) 财务口径裁定(逐字): | 区 | 判据 | 展示 | 禁止 | |---|---|---|---| | 实测区 | `data_status` ∈ {verified, pending, calculated} 且 `source_type` ≠ forecast | 正常展示 | 不得含 estimated | | 预估区 | `data_status = estimated` 或 `source_type = forecast` | 单列区 / 带「预估」标签 | 禁与实测相加、禁进趋势序列、禁作预警依据 | 实现口径说明(**边界决策,供复核**): - 本模块按**预估区的补集**实现实测区(`data_status='estimated'` 或 `source_type='forecast'` 一律排除),而不是按 `{verified, pending, calculated}` 正向白名单实现。 理由两条:①裁定的「禁止」列只点名 `estimated`;②本单验收要求 「回归:实测区各端点返回值与改动前逐期等值(**仅少掉 2 行预估**)」, 正向白名单会连带排除 `invalid`(6) / `error`(6) 行,超出该回归口径。 若产品口径要求把 invalid/error 也逐出取数层,属**另一次口径变更**,须财务重新裁定。 - `NULL` 值按实测处理(`coalesce(col, '')`):历史行 `data_status` 可能为空, 空值不等于「预估」(fail-open 到既有行为,不静默吞行)。 约定:所有对外 `/api/cma/**` 的 KPIValue 读路径**默认只取实测区**; 仅当调用方显式传 `include_forecast=true` 时,预估行才**以独立字段** (`forecast_value` / `is_forecast` / `forecast_rows`)返回,**不与实测相加**。 禁止在调用点各写一份 `data_status == 'estimated'` 字面量(避免多处漂移)。 """ from sqlalchemy import and_, func, or_ from app.models import KPIValue #: 预估区 —— 数据状态判据 FORECAST_DATA_STATUS = "estimated" #: 预估区 —— 来源类型判据 FORECAST_SOURCE_TYPES = ("forecast",) def _non_null_neq(column, value): """`coalesce(column, '') != value` —— NULL 视为「非该值」(不静默吞历史空值行)""" return func.coalesce(column, "") != value def actual_only_clause(): """实测区 SQL 判据 = 预估区补集(对外读路径默认过滤条件)""" return and_( _non_null_neq(KPIValue.data_status, FORECAST_DATA_STATUS), *[_non_null_neq(KPIValue.source_type, t) for t in FORECAST_SOURCE_TYPES], ) def forecast_only_clause(): """预估区 SQL 判据(`include_forecast=true` 时单列取数用)""" return or_( KPIValue.data_status == FORECAST_DATA_STATUS, KPIValue.source_type.in_(FORECAST_SOURCE_TYPES), ) def is_forecast_row(row) -> bool: """Python 侧同判据(单一事实源,避免调用点重写字面量)""" if row is None: return False data_status = getattr(row, "data_status", None) or "" source_type = getattr(row, "source_type", None) or "" return (data_status == FORECAST_DATA_STATUS or source_type in FORECAST_SOURCE_TYPES)