From c3ff345d0adef142c46f24d9cff90066994f20c5 Mon Sep 17 00:00:00 2001 From: wecom-fullstack Date: Fri, 25 Sep 2026 07:07:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(bep):=20=E6=B4=BE=E7=94=9F=E5=99=A8?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=9D=99=E9=BB=98=E9=99=8D=E7=BA=A7=E5=B7=B2?= =?UTF-8?q?=E8=BD=AC=E6=AD=A3=E8=A1=8C=EF=BC=88ASK-20260924-10=20=E4=BB=BB?= =?UTF-8?q?=E6=80=BB=E6=94=BE=E8=A1=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - upsert 分支原无条件 existing.data_status='pending' ⇒ 人工转正次日 06:35 被打回 pending - 值未变/值变一律不写 data_status(保留既有,含 verified) - 值变:remark 追加「值已更新(batch=…, 旧X → 新Y) 待财务复核」+ 落复核提示件(_reports/,不进队列根) - 新期间仍写 pending(9-14 复核流入口不变);算式/TARGET_KPIS/阈值逻辑不动 --- backend/scripts/kpi_bep_deriver.py | 73 ++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/backend/scripts/kpi_bep_deriver.py b/backend/scripts/kpi_bep_deriver.py index c1bc59ea..b770c150 100644 --- a/backend/scripts/kpi_bep_deriver.py +++ b/backend/scripts/kpi_bep_deriver.py @@ -17,7 +17,10 @@ 结账信号优先:DB 损益结转凭证(carry_forward=1 & subject_code='3103') → check.md 文件; 两者皆无时才退回旧「销售额阈值」兜底(销售低 ≠ 未结账,仅保底用,日志标注依据) - 单位: voucher 金额为元,聚合后 /10000 → 万元 -- 幂等: 同 kpi_id+period+source_type='derived' 存在则更新,可安全重跑 +- 幂等: 同 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 逻辑 用法: @@ -68,6 +71,54 @@ 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 过滤兜底)""" @@ -282,6 +333,7 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, batch = f"bep-{datetime.now().strftime('%Y%m%d%H%M%S')}" written = {} value_rows = {} + changed = [] for code, _ in TARGET_KPIS: kpi = kpis[code] if code in CASH_CODES: @@ -297,10 +349,22 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, 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 - existing.remark = remark - existing.data_status = "pending" + 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, @@ -359,6 +423,8 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, status="pending", )) db.commit() + # 值变留痕(ASK-20260924-10):落复核提示件(报告类 → _reports/,不进队列根) + _write_value_changed_note(entity_id, period, batch, changed) return { "entity_id": entity_id, @@ -377,6 +443,7 @@ def run_derive(db, entity_id: int, period: str, rebate_rate_pct: float, "cm_pct": round(cm_pct, 2), "values": values, "written": written, + "value_changed": changed, }