feat(ar): 逾期预警判据 G5——90-150 前池 5pp/10万 + 责任人点名优先(阶段B 受批面)
ci/woodpecker/push/main Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
This commit is contained in:
@@ -34,34 +34,18 @@ from app.utils import ar_caliber # noqa: E402
|
||||
RULE_TYPE = "ar_overdue"
|
||||
DEFAULT_KPI_CODE = "F_AR_002" # 逾期应收账款率(财务口径);库内暂缺时须由财务定绑定
|
||||
|
||||
# ── 业务数字唯一定义处(阶段B 放行后如需调阈值:改这里或直接改 DB 行)──
|
||||
# ── 业务数字唯一定义处(阶段B G5 2026-10-21 裁定:停用 150+ 占比,改 90–150 前池)──
|
||||
DEFAULT_PARAMS = {
|
||||
"kpi_code": DEFAULT_KPI_CODE,
|
||||
"caliber": ar_caliber.CALIBER_DEBIT,
|
||||
"red": {
|
||||
# 2026-10-20 财务裁决:90 天占比红线停用(10 期 10/10 恒红、无区分度)→ 键位保留、置 null
|
||||
"share_90plus_gt": None,
|
||||
"single_overdue_amt_gt": 50000,
|
||||
"single_overdue_min_days": 30,
|
||||
"count_mom_pct_gte": 0.20,
|
||||
# 150+ 档占比环比上升 = 结构恶化(新增红线 2)
|
||||
"structure_worsen": True,
|
||||
},
|
||||
"yellow": {
|
||||
"share_90plus_min": 0.15,
|
||||
"share_90plus_max": 0.25,
|
||||
"single_overdue_amt_min": 10000,
|
||||
"single_overdue_amt_max": 50000,
|
||||
},
|
||||
# 责任人:逐户派生「近 3 个月主业务员 → 部门负责人」,两路皆失败则该户不生成(skipped[])
|
||||
"assignee_source": {
|
||||
"mode": "salesman_then_dept",
|
||||
"lookback_months": 3,
|
||||
"dept_fallback": {"渠道": 118, "商务": 114, "直属酒窖": 117},
|
||||
},
|
||||
"share_90plus_gt": None,
|
||||
"share_150p_gt": None,
|
||||
"share_90_150_rise_pp_min": 5.0,
|
||||
"share_90_150_amt_min": 100000,
|
||||
"overdue_amt_display_only": True,
|
||||
"single_overdue_amt_gt": 50000,
|
||||
"assignee_by_unit": {},
|
||||
"exclude_unit_codes": [],
|
||||
"action_texts": {"red": "催收+停供评估", "yellow": "对账+催收"},
|
||||
"assignee_source": {"aggregate_assignee": "", "escalate": "任富海"},
|
||||
}
|
||||
|
||||
|
||||
@@ -81,9 +65,20 @@ def main() -> int:
|
||||
ap.add_argument("--assignee", default=None, help="覆盖 assignee_default(财务指定责任人)")
|
||||
ap.add_argument("--apply", action="store_true", help="真正写库(阶段B 才允许)")
|
||||
ap.add_argument("--emit-params", default=None, help="把最终 params JSON 写入指定文件")
|
||||
ap.add_argument("--params-file", default=None,
|
||||
help="整份 params JSON(与 DB 行逐字一致;给了就覆盖 DEFAULT_PARAMS)")
|
||||
args = ap.parse_args()
|
||||
|
||||
params = json.loads(json.dumps(DEFAULT_PARAMS)) # deep copy
|
||||
if args.params_file:
|
||||
with open(args.params_file, encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
params = raw.get("params", raw) if isinstance(raw, dict) else raw
|
||||
if not isinstance(params, dict):
|
||||
print("[BLOCK] --params-file 必须是 JSON 对象或 {params:{...}}", file=sys.stderr)
|
||||
return 3
|
||||
params = json.loads(json.dumps(params, ensure_ascii=False)) # deep copy
|
||||
else:
|
||||
params = json.loads(json.dumps(DEFAULT_PARAMS)) # deep copy
|
||||
if args.assignee is not None:
|
||||
params["assignee_default"] = args.assignee
|
||||
if args.emit_params:
|
||||
@@ -125,7 +120,8 @@ def main() -> int:
|
||||
"params": params,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
src = params.get("assignee_source") or {}
|
||||
if not params.get("assignee_default") and not src.get("mode"):
|
||||
if not (params.get("assignee_default") or params.get("assignee_by_unit")
|
||||
or src.get("mode") or src.get("aggregate_assignee")):
|
||||
print("[WARN] 责任人均未配置 → 运行期 fail-closed 400(阶段B 前须由财务给值)")
|
||||
|
||||
if exist:
|
||||
|
||||
Reference in New Issue
Block a user