fix(bep): pick target period by period-closed signal, sales threshold as fallback only
Defect: _pick_target_period treated "month sales < 30% of 6-month avg" as
"period not closed", so 2026-08 (actually closed: 3103 carry-forward vouchers
present) was skipped because of a low-sales off-season month, pinning the five
F_BEP* KPIs at 2026-07.
Fix: add _is_period_closed() - primary signal voucher_details
(carry_forward=1 AND subject_code='3103'), secondary signal
ZXT_LANDING_DIR/{YYYY.MM}/check.md containing both "issue PASS" and
"P&L carry-forward PASS". _pick_target_period now scans candidate months desc
and returns the first closed one, with no sales filtering; the old sales
threshold path is kept only as a fallback (warns "sales-threshold fallback").
Scope: run_derive() formulas, collector, kpi_definitions and cron untouched.
Verified: period=2026-08; 167.5 / 60.4 / 39.6 / 118.7 / 85.2;
F_BEP_COVER 2026-08 red pending, 2026-07 pending resolved; idempotent on re-run.
This commit is contained in:
@@ -13,7 +13,9 @@
|
||||
- 折旧: subject_code = '560213'(管理费用-折旧叶子) 借方合计,YTD 月均 → 现金口径剔除项(eq精确匹配,防5602130误含)
|
||||
- 渠补率: 厂家政策定死 84.61% (571103渠补支出/5001销售, 1-7月累计凭证验证≈84.0%~84.6%)
|
||||
- 边际贡献率 = 1 - 渠补率 = 15.39%
|
||||
- 目标月: --period 显式指定;默认自动选最近完整月(当月跳过;最近月销售额<近6月均30%视为未结账回退)
|
||||
- 目标月: --period 显式指定;默认自动选最近「已结账」完整月(当月跳过)。
|
||||
结账信号优先:DB 损益结转凭证(carry_forward=1 & subject_code='3103') → check.md 文件;
|
||||
两者皆无时才退回旧「销售额阈值」兜底(销售低 ≠ 未结账,仅保底用,日志标注依据)
|
||||
- 单位: voucher 金额为元,聚合后 /10000 → 万元
|
||||
- 幂等: 同 kpi_id+period+source_type='derived' 存在则更新,可安全重跑
|
||||
- 只写 source_type='derived' 行;不触碰 auto_collect/manual/excel/ledger 行;不改 collector 逻辑
|
||||
@@ -59,6 +61,10 @@ 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"
|
||||
|
||||
|
||||
def _month_range(period: str):
|
||||
@@ -101,8 +107,49 @@ def _month_sales(db, entity_id: int, period: str) -> float:
|
||||
("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:
|
||||
"""自动选择目标月:跳过当月;最近月销售额<近6月完整月均30%则视为未结账回退"""
|
||||
"""自动选择目标月:跳过当月。
|
||||
|
||||
判定顺序(2026-09-12 修订):
|
||||
1. 结账信号优先 —— 按 desc 返回第一个 _is_period_closed 为真的月;
|
||||
结账成立时不再用销售额做任何筛选(销售低 ≠ 未结账,历史缺陷根因)
|
||||
2. 所有候选月均无结账信号 → 退回旧「销售额 < 近6月均×30%」兜底(日志标注『依据销售额兜底』)
|
||||
"""
|
||||
# 收集已有数据月份(5001 贷方)
|
||||
rows = db.query(VoucherDetail.period).filter(
|
||||
VoucherDetail.entity_id == entity_id,
|
||||
@@ -121,6 +168,17 @@ def _pick_target_period(db, entity_id: int, now: date) -> str:
|
||||
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:
|
||||
@@ -137,8 +195,10 @@ def _pick_target_period(db, entity_id: int, now: date) -> str:
|
||||
if s <= 0:
|
||||
continue # 该月无销售,往前找
|
||||
if ref_avg and s < ref_avg * 0.3:
|
||||
logger.info("目标月 %s 销售 %.1f万 < 参考月均 %.1f万*30%%,视为未结账,回退", p, s, ref_avg)
|
||||
logger.info("目标月 %s 销售 %.1f万 < 参考月均 %.1f万*30%%,依据销售额兜底判定未结账,回退",
|
||||
p, s, ref_avg)
|
||||
continue
|
||||
logger.info("目标月 %s 依据销售额兜底采用", p)
|
||||
return p
|
||||
return candidates[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user