- kpi_value_collector: 所有消费 voucher_details 的归集路径统一排除已作废凭证 (NOT EXISTS voucher_void_marks, entity_id+voucher_no+voucher_date+revoked_at IS NULL), 与 DB 视图 v_voucher_details_active 定义一致;标记表不存在时自动跳过(兼容旧库) - 禁硬编码:凭证号不出现在代码/配置,撤单只需写标记表 - verify_cashflow_direct_caliber: 2026-08 从已知差异放行改为硬断言(8 期逐期一致) - tests/test_void_marker_filter_20260914.py: 标记表在/不在 两分支 3 例
132 lines
6.5 KiB
Python
132 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
||
"""回归电池: 现金流量表直接法口径(cashflow_direct)—— KPI4 F_OP_CFLOW / KPI45 F_FCF。
|
||
|
||
任务: yanxue-budget-cashflow-kpi-activate-20260914(财务口径说明 §二/§三/§五 落地复核)
|
||
|
||
判据(全部只读,write_kpi=False):
|
||
1. 生产同一代码路径试算 2026-01..08,与现网 zxt-sync 权威值对账(容差 0.001)
|
||
—— 2026-08 差异已随「作废/撤单标记过滤」接入归集器(2026-09-14)消除:
|
||
归集侧统一按 cma.voucher_void_marks 排除已作废凭证,8 期逐期一致(无放行期)
|
||
2. 恒等式: 经营净额 + 筹资净额 + 投资净额 = 现金总净额(每期 |Δ| ≤ 0.001)
|
||
3. 负例对照: 若按「凭证级整单分类」(错误做法),2026-06 应为 453,697.63 元
|
||
(行级 = 434,997.63 元,差 +18,700 = 凭证 31226 同单内的银行手续费)→ 证明必须行级拆分
|
||
4. F_FCF = 经营净额 − 资本支出;酣客 2026 年 capex=0 → 与经营净额同值
|
||
|
||
用法: /usr/bin/python3 backend/scripts/verify_cashflow_direct_caliber.py (退出码 0 = 全部通过)
|
||
"""
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from sqlalchemy import text
|
||
from app.database import get_session_local
|
||
from app.models import KPIValueSource, KPIValue
|
||
from scripts.kpi_value_collector import collect_for_mapping
|
||
|
||
ENTITY = 1
|
||
PERIODS = ["2026-0%d" % i for i in range(1, 9)]
|
||
TOL = 0.001
|
||
KNOWN_DIFF_PERIODS = set() # 2026-09-14 起为空集:作废标记过滤已接入归集器,8 期逐期硬断言
|
||
RULE = {
|
||
"calc": "cashflow_direct", "measure": "net", "version": 1, "bucket": "operating",
|
||
"cash_subjects": ["1001%", "1002%", "1012%"],
|
||
"financing_counterpart": ["2001", "2501", "2502", "3001", "3002", "3101", "3103", "3104", "2232", "2241"],
|
||
"investing_counterpart": ["1501", "1511", "1601", "1602", "1603", "1604", "1605", "1606", "1701", "1702"],
|
||
"round": 4,
|
||
}
|
||
CAPEX = {"counterpart": ["1601", "1604", "1605", "1606", "1701", "1702"], "side": "debit"}
|
||
CASH_PREFIX = ("1001", "1002", "1012")
|
||
FIN = tuple(RULE["financing_counterpart"])
|
||
INV = tuple(RULE["investing_counterpart"])
|
||
|
||
fails, warns = [], []
|
||
|
||
|
||
def probe(db, kpi_id, period, rule):
|
||
m = KPIValueSource(entity_id=ENTITY, kpi_id=kpi_id, source_table="voucher_details",
|
||
source_field="credit_amount", aggregate="sum", filter_rule=rule,
|
||
period_field="period", unit_conversion=0.0001, status="active")
|
||
return collect_for_mapping(db, m, period, write_kpi=False)
|
||
|
||
|
||
def ref(db, kpi_id, period):
|
||
row = db.query(KPIValue).filter(
|
||
KPIValue.kpi_id == kpi_id, KPIValue.period == period,
|
||
KPIValue.source_type == "zxt-sync", KPIValue.actual_value.isnot(None),
|
||
).order_by(KPIValue.calculated_at.desc()).first()
|
||
return float(row.actual_value) if row else None
|
||
|
||
|
||
def main():
|
||
db = get_session_local()()
|
||
try:
|
||
print("== 判据1: 8 期对账(现网 zxt-sync)==")
|
||
for p in PERIODS:
|
||
for kpi in (4, 45):
|
||
rule = dict(RULE, minus_capex=CAPEX) if kpi == 45 else dict(RULE)
|
||
val, _ = probe(db, kpi, p, rule)
|
||
r = ref(db, kpi, p)
|
||
ok = val is not None and r is not None and abs(val - r) <= TOL
|
||
tag = "PASS" if ok else ("WARN" if p in KNOWN_DIFF_PERIODS else "FAIL")
|
||
print(f" {tag} KPI{kpi} {p}: 算={val} 现网={r}")
|
||
if not ok:
|
||
(warns if p in KNOWN_DIFF_PERIODS else fails).append(f"KPI{kpi} {p}")
|
||
|
||
print("\n== 判据2: 恒等式(经营+筹资+投资=现金总净额)& 判据4: FCF=capex扣减 ==")
|
||
for p in PERIODS:
|
||
op, _ = probe(db, 4, p, dict(RULE, bucket="operating"))
|
||
fin, _ = probe(db, 4, p, dict(RULE, bucket="financing"))
|
||
inv, _ = probe(db, 4, p, dict(RULE, bucket="investing"))
|
||
net, _ = probe(db, 4, p, dict(RULE, bucket="net_cash"))
|
||
fcf, _ = probe(db, 45, p, dict(RULE, minus_capex=CAPEX))
|
||
f = lambda x: float(x) if x is not None else 0.0
|
||
ident = abs(f(op) + f(fin) + f(inv) - f(net))
|
||
same = abs(f(fcf) - f(op)) <= TOL # 酣客 capex=0 → FCF ≡ 经营净额
|
||
ok = ident <= TOL and same
|
||
print(f" {'PASS' if ok else 'FAIL'} {p}: 经营={op} 筹资={fin} 投资={inv} 现金净额={net} "
|
||
f"identityΔ={round(ident, 4)} FCF={fcf}")
|
||
if not ok:
|
||
fails.append(f"identity/{p}")
|
||
|
||
print("\n== 判据3: 负例对照(凭证级整单分类 = 错误做法)==")
|
||
sql = text("""
|
||
SELECT v.voucher_no, v.subject_code, v.debit_amount, v.credit_amount
|
||
FROM voucher_details v
|
||
JOIN (SELECT DISTINCT entity_id, voucher_date, voucher_no, period FROM voucher_details
|
||
WHERE entity_id=:e AND period=:p
|
||
AND (subject_code LIKE '1001%' OR subject_code LIKE '1002%' OR subject_code LIKE '1012%')) w
|
||
ON w.entity_id=v.entity_id AND w.voucher_date=v.voucher_date
|
||
AND w.voucher_no=v.voucher_no AND w.period=v.period
|
||
WHERE v.entity_id=:e AND v.period=:p""")
|
||
rows = db.execute(sql, {"e": ENTITY, "p": "2026-06"}).fetchall()
|
||
line_op = vlevel_op = 0.0
|
||
per_voucher = {}
|
||
for vno, code, dr, cr in rows:
|
||
code = str(code)
|
||
n = float(cr) - float(dr)
|
||
per_voucher.setdefault(vno, []).append((code, n))
|
||
if code.startswith(CASH_PREFIX):
|
||
continue
|
||
if code[:4] not in FIN and code[:4] not in INV:
|
||
line_op += n
|
||
for vno, lines in per_voucher.items():
|
||
cps = [(c, n) for c, n in lines if not c.startswith(CASH_PREFIX)]
|
||
if any(c[:4] in FIN or c[:4] in INV for c, _ in cps):
|
||
continue
|
||
vlevel_op += sum(n for _, n in cps)
|
||
ok = abs(line_op - 434997.63) <= 0.01 and abs(vlevel_op - line_op - 18700.0) <= 0.01
|
||
print(f" {'PASS' if ok else 'FAIL'} 2026-06 行级={round(line_op, 2)} 凭证级={round(vlevel_op, 2)} "
|
||
f"Δ={round(vlevel_op - line_op, 2)}(预期行级 434,997.63 / Δ +18,700.00)")
|
||
if not ok:
|
||
fails.append("negative-control-2026-06")
|
||
|
||
print(f"\n===== 汇总: {'全部通过' if not fails else 'FAIL ' + str(len(fails))} "
|
||
f"| WARN(已知差异) {len(warns)} 项 {warns if warns else ''} =====")
|
||
return 1 if fails else 0
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|