"""保本点(盈亏平衡)派生KPI执行器 — 管理会计OS (cma-bep-kpi 2026-09-08) 按财务Bot权威口径(陕西酣客_保本点分析_2026年1-7月.md)自动计算并落库五个派生KPI: F_BEP_REVENUE 保本销售额(万元) = 月均管理费 ÷ 边际贡献率 (账面口径,含折旧) F_BEP_COVER 保本覆盖率(%) = 月均销售 ÷ 保本销售额 × 100 (正向: >=100绿 >=85黄 <85红) F_BEP_GAP 保本缺口(%) = (保本销售额-月均销售) ÷ 保本销售额 × 100 (反向: <=0绿 <=15黄 >15红) F_BEP_REVENUE_CASH 现金保本销售额(万元) = (月均管理费-月均折旧) ÷ 边际贡献率 (现金口径,剔除折旧560213,中性无阈值) F_BEP_COVER_CASH 现金保本覆盖率(%) = 月均销售 ÷ 现金保本销售额 × 100 (正向: >=100绿 >=85黄 <85红) F_BEP_REBATE 实测渠补率(%) = Σ(571103借方) ÷ Σ(5001贷方) × 100 (YTD累计; 卡面单一事实源,无阈值) 口径(与财务Bot报告一致): - 销售收入: voucher_details.subject_code='5001'(主营业务收入) 贷方合计,当年1月至目标月 YTD 月均 - 管理费: subject_code LIKE '5602%'(管理费用,含560213折旧) 借−贷净额合计(carry_forward=0),YTD 月均 → 账面口径 - 折旧: subject_code = '560213'(管理费用-折旧叶子) 借−贷净额合计(carry_forward=0),YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含) - 渠补率: **实测累计口径**(2026-10-02 财务裁定,任总 11:43 CST 放行「改,这样数据更加真实」) 渠补率(实测) = Σ(571103 借方) ÷ Σ(5001 贷方) × 100,期间 = 年初至当期(YTD),保留 2 位小数。 `POLICY_REBATE_RATE_PCT = 84.61` 政策常量**仅作对照展示**(写进返回值 rebate_policy_pct / remark), **不参与任何计算**;`--rebate-rate` 显式传值时视为人工诊断覆盖(正常调用不再使用)。 - 边际贡献率 = 100 − 渠补率(实测) - fail-closed 边界(2026-10-02 裁定):某期 571103 或 5001 缺数/为 0 ⇒ **该期不出保本值** (六条派生行写 actual_value=NULL + data_status='pending' + remark 注明「渠补率不可得」), **不得静默回落 84.61 常量**;累计口径天然平滑单月波动(8 月单月 571103/5001=134% 不影响累计值)。 - 目标月: --period 显式指定;默认自动选最近「已结账」完整月(当月跳过)。 结账信号优先:DB 损益结转凭证(carry_forward=1 & subject_code='3103') → check.md 文件; 两者皆无时才退回旧「销售额阈值」兜底(销售低 ≠ 未结账,仅保底用,日志标注依据) - 单位: voucher 金额为元,聚合后 /10000 → 万元 - 幂等: 同 kpi_id+period+source_type='derived' 存在则更新,可安全重跑; **不降级**(ASK-20260924-10 / 2026-09-24):值未变或值变都不回写 data_status, 保留既有状态(含人工/任总放行的 verified),避免「人工转正 → 次日派生静默打回 pending」。 值变时 remark 留痕并落复核提示件(见 _reports/fullstack--bep-value-changed-review.md)。 - 只写 source_type='derived' 行;不触碰 auto_collect/manual/excel/ledger 行;不改 collector 逻辑 用法: venv/bin/python3 scripts/kpi_bep_deriver.py # 自动选目标月 venv/bin/python3 scripts/kpi_bep_deriver.py --period 2026-07 # 指定期间(实跑验证用) venv/bin/python3 scripts/kpi_bep_deriver.py --entity 1 --rebate-rate 84.61 venv/bin/python3 scripts/kpi_bep_deriver.py --dry-run # 只计算不落库 """ import argparse import logging import os import sys from datetime import datetime, date sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import re from sqlalchemy import func from app.database import get_session_local from app.models import KPIValue, KPIDefinition, VoucherDetail, KPIAlert # 裁定 R1(2026-09-19 财务口径):demo 账套不产生业务预警(派生值仍落库,仅跳过预警同步) from app.utils.alert_entity_scope import is_excluded_entity logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("cma.bep_deriver") # 厂家政策渠补率(定死不可谈判,财务Bot报告权威值 2026-09-08) # ⚠️ 2026-10-02 起**仅作对照展示**(返回值 rebate_policy_pct / remark),不参与任何计算。 # 参与计算的渠补率 = 逐期实测累计(见 compute_rebate_rate / 文件头「渠补率」条)。 POLICY_REBATE_RATE_PCT = 84.61 # 派生KPI 编码 → (kpi_code, 名称) TARGET_KPIS = [ ("F_BEP_REVENUE", "保本销售额"), ("F_BEP_COVER", "保本覆盖率"), ("F_BEP_GAP", "保本缺口"), ("F_BEP_REVENUE_CASH", "现金口径保本销售额"), ("F_BEP_COVER_CASH", "现金口径保本覆盖率"), ("F_BEP_REBATE", "实测渠补率"), ] # 实测渠补率 KPI 编码(卡面单一事实源;无阈值,不参与预警级别判定) REBATE_KPI = "F_BEP_REBATE" # 保本口径 KPI(缺 F_BEP_REBATE 定义时不阻断派生主流程,仅跳过渠补率落库并告警) CORE_KPI_CODES = [c for c, _ in TARGET_KPIS if c != REBATE_KPI] # 现金口径 KPI 编码(计算路径独立于账面,remark 文案用现金口径) CASH_CODES = {"F_BEP_REVENUE_CASH", "F_BEP_COVER_CASH"} SALES_CODE = "5001" # 主营业务收入 ADMIN_PREFIX = "5602" # 管理费用(含折旧叶子560213) DEP_SUBJECT = "560213" # 管理费用-折旧(现金口径剔除项,eq 精确匹配防 5602130 误含) REBATE_CODE = "571103" # 渠补/捐赠支出(校验用,不直接进公式) CARRY_SUBJECT = "3103" # 本年利润(carry_forward=1 且该科目 = 该期损益结转凭证) # 智享通落盘目录(与 app/api/data.py 的 ZXT_LANDING_DIR 一致)——结账信号次选来源 ZXT_LANDING_DIR = "/root/WB_WS/_data/zxt-hanke" # ── ASK-20260924-10(2026-09-24 任总放行):派生器不得静默降级已转正行 ────────── # 缺陷(原实现):upsert 分支无条件 `existing.data_status = "pending"`(与值是否变化无关) # ⇒ 人工/任总放行的值转正为 verified 后,次日 06:35 派生器一跑就被打回 pending, # 看板 / 预警 / Bot 全链路再次读不到,交付在 24 小时内静默消失。 # 修正口径(本单指定,属待财务复核的裁定点): # · 值未变 → 不写 data_status(保留既有,含 verified);source_batch/remark/actual_value 照旧更新 # · 值变 → 同样不写 data_status(不降级,避免看板数字忽现忽隐) # + remark 追加「值已更新(batch=…, 旧X → 新Y) 待财务复核」+ 落复核提示件 # · 新期间(原本无行)→ 维持 pending(9-14 复核流入口不变) # 提示件为**报告类**,落 `_reports/` 而非对方队列根(避免误唤醒其他 Bot); # 落点可用环境变量 BEP_REVIEW_NOTE_DIR 覆盖(沙箱/测试用)。 DEFAULT_REVIEW_NOTE_DIR = "/root/.hermes/profiles/wecom-finance/pending-tasks/_reports" def _review_note_dir() -> str: return os.environ.get("BEP_REVIEW_NOTE_DIR") or DEFAULT_REVIEW_NOTE_DIR def _write_value_changed_note(entity_id: int, period: str, batch: str, changed: list): """值变时落「待财务复核」提示件。写失败只告警,绝不阻断派生主流程。""" if not changed: return None try: d = _review_note_dir() os.makedirs(d, exist_ok=True) path = os.path.join(d, "fullstack-%s-bep-value-changed-review.md" % datetime.now().strftime("%Y%m%d")) lines = [ "# 保本派生值变动 · 待财务复核(派生器自动生成;报告类产物,非队列任务)", "", "- 生成器: `backend/scripts/kpi_bep_deriver.py`(值变路径,ASK-20260924-10 口径)", "- entity_id: %s | period: %s | batch: %s" % (entity_id, period, batch), "- 生成时刻: %s" % datetime.now().strftime("%F %T"), "- 口径: 值变**不降级** data_status(保留既有,含 verified),仅 remark 留痕 + 请财务复核", "", "| kpi_code | 旧值 | 新值 | data_status(写后) |", "|---|---|---|---|", ] for c in changed: lines.append("| %s | %s | %s | %s |" % (c["code"], c["old"], c["new"], c["status"])) lines += ["", "> 本件落在 `_reports/`(报告类),不进队列根,避免误唤醒其他 Bot。", ""] with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) logger.warning("值变留痕:复核提示件已落 %s(%d 个 KPI)", path, len(changed)) return path except Exception as e: # 提示件失败不影响派生落库 logger.error("值变复核提示件写入失败(不阻断派生): %s", e) return None def _month_range(period: str): """返回该期间当年1月1日到月末的日期边界(用于 voucher_date 过滤兜底)""" y = int(period[:4]) m = int(period[5:7]) start = date(y, 1, 1) # 月末 if m == 12: end = date(y, 12, 31) else: end = date(y, m + 1, 1).replace(day=1) # 次月1日 from datetime import timedelta end = end - timedelta(days=1) return start, end def _sum_field(db, entity_id: int, period_from: str, period_to: str, field: str, subject_filter=None) -> float: """聚合 voucher_details 金额(元)。subject_filter: ('eq', code) / ('like', prefix) field="net" = 借−贷 净额(口径4:费用/损益一律净额)。 ⚠️ 净额只在 carry_forward==0 行域成立(含结转腿时损益类净额恒 0) —— 见 /srv/hermes/wisdom/_decisions/cma-expense-profit-net-caliber-20261002.md。 """ expr = (VoucherDetail.debit_amount - VoucherDetail.credit_amount if field == "net" else getattr(VoucherDetail, field)) q = db.query(func.sum(expr)).filter( VoucherDetail.entity_id == entity_id, VoucherDetail.carry_forward == 0, VoucherDetail.period >= period_from, VoucherDetail.period <= period_to, ) if subject_filter: kind, code = subject_filter if kind == "eq": q = q.filter(VoucherDetail.subject_code == code) elif kind == "like": q = q.filter(VoucherDetail.subject_code.like(code + "%")) val = q.scalar() return float(val or 0.0) def compute_rebate_rate(db, entity_id: int, period_from: str, period_to: str) -> dict: """实测累计渠补率(2026-10-02 财务裁定口径)。 渠补率(实测) = Σ(571103 借方) ÷ Σ(5001 贷方) × 100,期间 = 年初至当期(YTD),保留 2 位小数。 · 571103 = 渠补/捐赠支出(损益类费用科目)⇒ 本期发生额落**借方**; · 5001 = 主营业务收入 ⇒ 本期发生额落**贷方**; · 行域 = carry_forward=0(与 _sum_field 一致,排除期间损益结转腿)。 返回 {'available', 'rebate_pct', 'rebate_sum_w', 'sales_sum_w', 'reason'}; **fail-closed**:分子或分母缺数/为 0 ⇒ available=False, rebate_pct=None (调用方不得回落政策常量,须按「渠补率不可得」处理)。 """ rebate_sum_w = _sum_field(db, entity_id, period_from, period_to, "debit_amount", ("eq", REBATE_CODE)) / 10000.0 sales_sum_w = _sum_field(db, entity_id, period_from, period_to, "credit_amount", ("eq", SALES_CODE)) / 10000.0 info = { "rebate_sum_w": round(rebate_sum_w, 2), "sales_sum_w": round(sales_sum_w, 2), "available": False, "rebate_pct": None, "reason": "", } if sales_sum_w <= 0: info["reason"] = "渠补率不可得:5001 贷方累计缺数或为 0" return info if rebate_sum_w <= 0: info["reason"] = "渠补率不可得:571103 借方累计缺数或为 0" return info info["available"] = True info["rebate_pct"] = round(rebate_sum_w / sales_sum_w * 100.0, 2) return info def _month_sales(db, entity_id: int, period: str) -> float: """单月销售收入(万元)——用于目标月完整度判断""" return _sum_field(db, entity_id, period, period, "credit_amount", ("eq", SALES_CODE)) / 10000.0 def _is_period_closed(db, entity_id: int, period: str) -> tuple: """判断该期间是否已结账(损益结转已完成)。返回 (bool, 依据说明)。 主信号(DB): voucher_details 存在 carry_forward=1 且 subject_code='3103'(本年利润) 的行 = 该期损益结转凭证已生成 次信号(文件): ZXT_LANDING_DIR/{YYYY.MM}/check.md 同时含「本期: PASS」与「损益结转: PASS」 均不成立 → (False, "")。任何读取异常均吞掉,不影响主流程。 """ # 主信号:DB 损益结转凭证 try: hit = db.query(VoucherDetail.id).filter( VoucherDetail.entity_id == entity_id, VoucherDetail.carry_forward == 1, VoucherDetail.subject_code == CARRY_SUBJECT, VoucherDetail.period == period, ).first() if hit: return True, "DB损益结转" except Exception as e: # 查询异常不阻断,降级到文件信号 logger.warning("结账信号(DB)查询失败 period=%s: %s", period, e) # 次信号:智享通落盘 check.md try: path = os.path.join(ZXT_LANDING_DIR, period.replace("-", "."), "check.md") if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: txt = f.read() if "本期: PASS" in txt and "损益结转: PASS" in txt: return True, "check.md" except Exception: pass # 文件缺失/读取失败一律忽略 return False, "" def _pick_target_period(db, entity_id: int, now: date) -> str: """自动选择目标月:跳过当月。 判定顺序(2026-09-12 修订): 1. 结账信号优先 —— 按 desc 返回第一个 _is_period_closed 为真的月; 结账成立时不再用销售额做任何筛选(销售低 ≠ 未结账,历史缺陷根因) 2. 所有候选月均无结账信号 → 退回旧「销售额 < 近6月均×30%」兜底(日志标注『依据销售额兜底』) """ # 收集已有数据月份(5001 贷方) rows = db.query(VoucherDetail.period).filter( VoucherDetail.entity_id == entity_id, VoucherDetail.period.like("%-__"), ).distinct().order_by(VoucherDetail.period.desc()).all() periods = [r[0] for r in rows if r[0] and len(r[0]) == 7] if not periods: logger.warning("voucher_details 无 period 数据,无法自动选月") return now.strftime("%Y-%m") # 过滤掉未来/当月(当月未结束数据不全) cur = now.strftime("%Y-%m") candidates = [p for p in periods if p < cur] if not candidates: candidates = [p for p in periods if p <= cur] if not candidates: candidates = periods # 1) 结账信号优先(DB损益结转 → check.md),命中即采用 for p in candidates: closed, reason = _is_period_closed(db, entity_id, p) if closed: logger.info("目标月 %s 判定已结账(依据:%s),采用", p, reason) return p # 2) 兜底:无任何结账信号时,退回旧销售额阈值逻辑 logger.warning("候选月 %s 均无结账信号(DB损益结转/check.md),退回销售额阈值兜底", ", ".join(candidates[:6])) # 参考月均:候选月中最近 6 个不同月的月销售 ref_months = [] for p in periods: if p >= cur: continue s = _month_sales(db, entity_id, p) ref_months.append((p, s)) if len(ref_months) >= 6: break ref_avg = (sum(s for _, s in ref_months) / len(ref_months)) if ref_months else None for p in candidates: s = _month_sales(db, entity_id, p) if s <= 0: continue # 该月无销售,往前找 if ref_avg and s < ref_avg * 0.3: logger.info("目标月 %s 销售 %.1f万 < 参考月均 %.1f万*30%%,依据销售额兜底判定未结账,回退", p, s, ref_avg) continue logger.info("目标月 %s 依据销售额兜底采用", p) return p return candidates[0] def _eval_static_threshold(value: float, kpi) -> tuple: """按 KPI 定义 threshold_green/yellow/red 判定级别(绿→黄→红,命中即止)。 与 alert_generator._check_threshold 语义一致。green=正常不落预警。""" for level, field in (("green", "threshold_green"), ("yellow", "threshold_yellow"), ("red", "threshold_red")): expr = getattr(kpi, field, None) if not expr: continue m = re.match(r"(>=|<=|>|<|=|!=)\s*([\d.]+)", str(expr).strip()) if not m: continue op, val = m.group(1), float(m.group(2)) hit = { ">=": value >= val, "<=": value <= val, ">": value > val, "<": value < val, "=": value == val, "!=": value != val, }[op] if hit: level_names = {"green": "正常", "yellow": "预警", "red": "紧急"} msg = (f"KPI当前值 {value:.2f},触发{level_names[level]}阈值 ({expr})") return level, msg return None, None def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float = None, write_kpi: bool = True) -> dict: """计算并落库六个派生KPI(账面3条+现金2条+实测渠补率1条)。返回结果摘要。 渠补率(2026-10-02 裁定):默认按 YTD 从 voucher_details **现算** (Σ571103借方 ÷ Σ5001贷方),政策常量 84.61% 仅作对照返回(rebate_policy_pct)。 显式传 rebate_rate_pct(`--rebate-rate`)时视为人工诊断覆盖。 fail-closed:实测渠补率不可得且无人工覆盖 ⇒ 该期不出保本值(六行 pending/NULL)。 """ kpis = {k.kpi_code: k for k in db.query(KPIDefinition).filter( KPIDefinition.entity_id == entity_id, KPIDefinition.kpi_code.in_([c for c, _ in TARGET_KPIS]), KPIDefinition.status == "active", ).all()} missing = [c for c in CORE_KPI_CODES if c not in kpis] if missing: raise RuntimeError(f"entity={entity_id} 缺少派生KPI定义: {missing}") if REBATE_KPI not in kpis: # 渠补率定义缺失不阻断保本主流程(部署顺序容错),但必须显式告警: # 卡面此时取不到 rebate_pct(单一事实源缺失),需尽快补 bep_kpi_defs_rebate SQL。 logger.error("entity=%s 缺少 %s 定义(见 scripts/sql/bep_kpi_defs_rebate_20261002.sql);" "本期跳过实测渠补率落库,卡面将判「渠补率不可得」", entity_id, REBATE_KPI) year = period[:4] period_from = f"{year}-01" period_to = period months = int(period[5:7]) # YTD 月数(1月=1 ... 7月=7) # 1. 取数(元 → 万元) sales_sum_w = _sum_field(db, entity_id, period_from, period_to, "credit_amount", ("eq", SALES_CODE)) / 10000.0 admin_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", ("like", ADMIN_PREFIX)) / 10000.0 dep_sum_w = _sum_field(db, entity_id, period_from, period_to, "net", ("eq", DEP_SUBJECT)) / 10000.0 sales_avg = sales_sum_w / months admin_avg = admin_sum_w / months dep_avg = dep_sum_w / months cash_admin_avg = admin_avg - dep_avg # 现金口径固定成本(剔除折旧纸面费用) # 2. 渠补率:默认逐期实测累计(不再取政策常量;常量仅对照) rebate_info = compute_rebate_rate(db, entity_id, period_from, period_to) calc_pct = rebate_info["rebate_pct"] if rebate_rate_pct is not None: rebate_info = dict(rebate_info, available=True, rebate_pct=round(float(rebate_rate_pct), 2), reason="人工覆盖(--rebate-rate)") logger.warning("渠补率使用人工覆盖值 %.2f%%(YTD 实算值 %s)", rebate_rate_pct, ("%.2f%%" % calc_pct) if calc_pct is not None else "不可得") elif not rebate_info["available"]: # fail-closed:不出保本值,绝不静默回落政策常量 84.61 logger.error("period=%s %s ⇒ 本期不出保本值(fail-closed,不回落政策常量)", period, rebate_info["reason"]) return _fail_closed_rebate_unavailable(db, entity_id, period, months, rebate_info, kpis, write_kpi) rebate_pct = rebate_info["rebate_pct"] cm_pct = 100.0 - rebate_pct # 边际贡献率(%) = 100 − 实测渠补率 # 3. 派生计算(万元 / %)——账面3条路径不变;现金口径在账面基础上剔除折旧 bep = admin_avg / (cm_pct / 100.0) if cm_pct > 0 else 0.0 cover = (sales_avg / bep * 100.0) if bep > 0 else 0.0 gap = ((bep - sales_avg) / bep * 100.0) if bep > 0 else 0.0 cash_bep = cash_admin_avg / (cm_pct / 100.0) if cm_pct > 0 else 0.0 cash_cover = (sales_avg / cash_bep * 100.0) if cash_bep > 0 else 0.0 values = { "F_BEP_REVENUE": round(bep, 1), "F_BEP_COVER": round(cover, 1), "F_BEP_GAP": round(gap, 1), "F_BEP_REVENUE_CASH": round(cash_bep, 1), "F_BEP_COVER_CASH": round(cash_cover, 1), "F_BEP_REBATE": round(rebate_pct, 2), } # 4. upsert kpi_values (source_type='derived',幂等) batch = f"bep-{datetime.now().strftime('%Y%m%d%H%M%S')}" written = {} value_rows = {} changed = [] for code, _ in TARGET_KPIS: kpi = kpis.get(code) if kpi is None: # 定义缺失(仅可能为 F_BEP_REBATE,见上方告警) written[code] = "no-definition" continue if code == REBATE_KPI: remark = (f"实测渠补率(YTD累计): Σ571103借方 {rebate_info['rebate_sum_w']:.2f}万 ÷ " f"Σ5001贷方 {rebate_info['sales_sum_w']:.2f}万 × 100 = {rebate_pct:.2f}%" f"(政策常量 {POLICY_REBATE_RATE_PCT:.2f}% 仅供对照,不参与计算)") elif code in CASH_CODES: remark = (f"现金口径: 管理费月均{admin_avg:.2f}万-折旧月均{dep_avg:.2f}万" f"=固定现金{cash_admin_avg:.2f}万 ÷ 边际贡献率{cm_pct:.2f}% → 保本{cash_bep:.2f}万" f"(实测渠补率{rebate_pct:.2f}%)") else: remark = (f"保本派生器: 销售月均{sales_avg:.1f}万/管理费月均{admin_avg:.1f}万/" f"实测渠补率{rebate_pct:.2f}%(政策{POLICY_REBATE_RATE_PCT:.2f}%仅对照)") existing = db.query(KPIValue).filter( KPIValue.kpi_id == kpi.id, KPIValue.period == period, KPIValue.source_type == "derived", ).first() if write_kpi: is_new = existing is None if existing: old_value = float(existing.actual_value) if existing.actual_value is not None else None new_value = float(values[code]) value_changed = old_value is None or abs(old_value - new_value) > 1e-9 existing.actual_value = values[code] existing.source_batch = batch if value_changed: existing.remark = (f"{remark}|值已更新(batch={batch}, " f"旧{old_value} → 新{new_value}) 待财务复核") changed.append({"code": code, "old": old_value, "new": new_value, "status": existing.data_status}) else: existing.remark = remark # 2026-09-24 ASK-20260924-10:**不再**无条件 `existing.data_status = "pending"`。 # 值未变 / 值变 一律不写 data_status ⇒ 保留既有状态(含人工放行的 verified)。 # 原写法会让「人工转正」在次日 06:35 静默被打回 pending(看板数字忽现忽隐)。 # 新期间(下方 else 分支)仍写 pending,复核流入口不变。 else: existing = KPIValue( entity_id=entity_id, kpi_id=kpi.id, period=period, actual_value=values[code], source_type="derived", source_batch=batch, data_status="pending", remark=remark, ) db.add(existing) db.flush() value_rows[kpi.id] = existing written[code] = "insert" if is_new else "upsert" else: written[code] = "dry-run" if write_kpi: # 5. 同步预警: 仅对本派生KPI评估 threshold(green=正常不落 pending,yellow/red 落 pending)。 # 幂等规则:同一 KPIValue 已有同级别 pending → 保持不动(值未变不重建); # 其它/旧级别 pending → resolve;最新派生行永远只留一条当前级别。 # R1(裁定 2026-09-19):demo 账套不产生业务预警 → 整段预警同步跳过(派生值照落库)。 target_sync = [] if is_excluded_entity(db, entity_id) else TARGET_KPIS if not target_sync: logger.info("demo 账套(entity=%s)跳过预警同步(R1,裁定 2026-09-19)", entity_id) for code, _ in target_sync: kpi = kpis[code] kpi_value = value_rows.get(kpi.id) if not kpi_value: continue level, msg = _eval_static_threshold(kpi_value.actual_value, kpi) all_pending = db.query(KPIAlert).filter( KPIAlert.kpi_id == kpi.id, KPIAlert.status == "pending", ).all() same_value = [a for a in all_pending if a.kpi_value_id == kpi_value.id] if level in ("yellow", "red") and any(a.alert_level == level for a in same_value): # 当前值级别未变 → 只清其它旧值预警,不重建 for a in all_pending: if a.kpi_value_id != kpi_value.id: a.status = "resolved" a.resolution = "保本派生器: 派生值更新,旧预警自动解除" a.resolved_at = datetime.now() continue for a in all_pending: a.status = "resolved" a.resolution = "保本派生器: 派生值更新/级别变更,旧预警自动解除" a.resolved_at = datetime.now() if level in ("yellow", "red"): db.add(KPIAlert( entity_id=entity_id, kpi_id=kpi.id, kpi_value_id=kpi_value.id, alert_level=level, alert_message=f"{kpi.kpi_name}[{period}] {msg}", status="pending", )) db.commit() # 值变留痕(ASK-20260924-10):落复核提示件(报告类 → _reports/,不进队列根) _write_value_changed_note(entity_id, period, batch, changed) return { "entity_id": entity_id, "period": period, "months": months, "data_status": "ok", "sales_sum_w": round(sales_sum_w, 2), "sales_avg": round(sales_avg, 2), "admin_sum_w": round(admin_sum_w, 2), "admin_avg": round(admin_avg, 2), "dep_sum_w": round(dep_sum_w, 2), "dep_avg": round(dep_avg, 2), "cash_admin_avg": round(cash_admin_avg, 2), "rebate_sum_w": rebate_info["rebate_sum_w"], "rebate_sales_base_w": rebate_info["sales_sum_w"], "rebate_actual_pct": rebate_pct, "rebate_policy_pct": POLICY_REBATE_RATE_PCT, "rebate_source": rebate_info.get("reason") or "YTD实测累计(Σ571103借方/Σ5001贷方)", "rebate_calc_pct": round(calc_pct, 2) if calc_pct is not None else None, "cm_pct": round(cm_pct, 2), "values": values, "written": written, "value_changed": changed, } def _fail_closed_rebate_unavailable(db, entity_id: int, period: str, months: int, rebate_info: dict, kpis: dict, write_kpi: bool) -> dict: """fail-closed 落库:实测渠补率不可得 ⇒ 该期不出保本值。 六条派生行写 actual_value=NULL + data_status='pending' + remark 注明「渠补率不可得」, **不回落**政策常量 84.61(2026-10-02 财务裁定 / 任总 11:43 CST 放行)。 不评估阈值、不产生/解除预警(无值 ⇒ 无级别可评)。 """ reason = rebate_info["reason"] or "渠补率不可得" detail = (f"{reason};Σ571103借方 {rebate_info['rebate_sum_w']:.2f}万 / " f"Σ5001贷方 {rebate_info['sales_sum_w']:.2f}万 ⇒ 本期不出保本值(fail-closed," f"不回落政策常量 {POLICY_REBATE_RATE_PCT:.2f}%)") batch = f"bep-{datetime.now().strftime('%Y%m%d%H%M%S')}" written = {} if write_kpi: for code, _ in TARGET_KPIS: kpi = kpis.get(code) if kpi is None: written[code] = "no-definition" continue existing = db.query(KPIValue).filter( KPIValue.kpi_id == kpi.id, KPIValue.period == period, KPIValue.source_type == "derived", ).first() if existing is None: db.add(KPIValue( entity_id=entity_id, kpi_id=kpi.id, period=period, actual_value=None, source_type="derived", source_batch=batch, data_status="pending", remark=detail, )) written[code] = "insert" else: existing.actual_value = None existing.source_batch = batch existing.data_status = "pending" existing.remark = detail written[code] = "upsert" db.commit() else: written = {c: "dry-run" for c, _ in TARGET_KPIS} return { "entity_id": entity_id, "period": period, "months": months, "data_status": "pending", "rebate_unavailable": True, "reason": detail, "sales_sum_w": rebate_info["sales_sum_w"], "rebate_sum_w": rebate_info["rebate_sum_w"], "rebate_actual_pct": None, "rebate_policy_pct": POLICY_REBATE_RATE_PCT, "rebate_source": reason, "values": {}, "written": written, "value_changed": [], } def main(): ap = argparse.ArgumentParser(description="保本点派生KPI执行器") ap.add_argument("--period", default=None, help="目标期间 YYYY-MM(默认自动选最近完整月)") ap.add_argument("--entity", type=int, default=1, help="entity_id(默认1=陕西酣客)") ap.add_argument("--rebate-rate", type=float, default=None, help="渠补率%%人工覆盖(诊断用;**默认按 YTD 从 voucher_details 实测现算**,不再取政策常量)") ap.add_argument("--dry-run", action="store_true", help="只计算不落库") args = ap.parse_args() db = get_session_local()() try: period = args.period or _pick_target_period(db, args.entity, date.today()) r = run_derive(db, args.entity, period, args.rebate_rate, write_kpi=not args.dry_run) print(f"保本点派生计算完成 entity={r['entity_id']} period={r['period']} " f"(YTD {r['months']}个月) mode={'dry-run' if args.dry_run else 'write'} " f"data_status={r['data_status']}") if r["data_status"] == "pending": print(f" ⚠ fail-closed:{r['reason']}") print(f" 落库: {r['written']}") return print(f" 销售收入累计 {r['sales_sum_w']:.2f}万 月均 {r['sales_avg']:.2f}万") print(f" 管理费累计 {r['admin_sum_w']:.2f}万 月均 {r['admin_avg']:.2f}万 (含折旧,账面口径)") print(f" 折旧累计 {r['dep_sum_w']:.2f}万 月均 {r['dep_avg']:.2f}万 (560213) | " f"现金固定成本月均 {r['cash_admin_avg']:.2f}万 (管理费-折旧)") print(f" 渠补支出累计 {r['rebate_sum_w']:.2f}万 | 实测渠补率(YTD) {r['rebate_actual_pct']:.2f}% " f"| 政策常量 {r['rebate_policy_pct']:.2f}% (仅对照) | 边际贡献率 {r['cm_pct']:.2f}%") print(f" F_BEP_REVENUE 保本销售额 = {r['values']['F_BEP_REVENUE']} 万元") print(f" F_BEP_COVER 保本覆盖率 = {r['values']['F_BEP_COVER']} %") print(f" F_BEP_GAP 保本缺口 = {r['values']['F_BEP_GAP']} %") print(f" F_BEP_REVENUE_CASH 现金保本销售额 = {r['values']['F_BEP_REVENUE_CASH']} 万元") print(f" F_BEP_COVER_CASH 现金保本覆盖率 = {r['values']['F_BEP_COVER_CASH']} %") print(f" F_BEP_REBATE 实测渠补率 = {r['values']['F_BEP_REBATE']} %") print(f" 落库: {r['written']}") finally: db.close() if __name__ == "__main__": main()