fix(cpa): sales bill-type whitelist for customer revenue caliber (P0)
ci/woodpecker/push/main Pipeline failed

Customer revenue/concentration now filters bill_type IN (1,4,14) (XSD/XSTHD/XSHHD).

Non-sales docs (JHD purchase, YFKD prepay, YSKD accept, QTRK, SKD, YBFYD) carry SUPPLIER unit_id and were polluting 'customer concentration' (old Top1/Top2 were our biggest suppliers; root cause of 6.04x base gap).

Unify sign filter to (sum_money+sum_tax_money)>0 (equivalent to money_only on current data, diff 0 rows) and correct the withdrawn 407-row/17.2% wording in cpa_caliber docstring.

Deriver: anchors from cpa_caliber single source; new --concentration overwrite and --only concentration (minimal 9-row write face) with pre-overwrite backup.

Tests: fixture gains bill_type/sum_tax_money; +5 whitelist cases (44 passed targeted).
This commit is contained in:
Hermes CI Fix
2026-10-09 17:00:37 +08:00
parent 451c994a86
commit 1613f7f961
4 changed files with 298 additions and 79 deletions
+13 -5
View File
@@ -812,7 +812,7 @@ def run_forecast_deviation_check(
# ============================================================
def _customer_revenue_shares(db: Session, entity_id: int, lookback_months: int = 12, top_n: int = 1):
"""聚合客户(往来单位)收入占比 — 数据源 zxt_masterbill(智享通销售单据, unit_id=客户/往来单位)
"""聚合客户(往来单位)收入占比 — 数据源 zxt_masterbill(智享通**销售类**单据, unit_id=客户)
返回 (shares, total, period_range):
shares = [(unit_id, amount, pct), ...] 按金额倒序取前 top_n 名
@@ -820,9 +820,17 @@ def _customer_revenue_shares(db: Session, entity_id: int, lookback_months: int =
period_range = (start_date, end_date) ISO 字符串
无可用数据时返回 ([], 0.0, None)。
**口径单一事实源**:SQL 已抽到 `app/utils/cpa_caliber.py`(CPA 派生器共用同一实现,
防「同一客户收入两份结果」)。行为与抽函数前逐字一致(同过滤、同排序、同四舍五入)。
用独立连接执行,避免表不存在/方言差异污染调用方 session 事务。
**口径单一事实源**:SQL 在 `app/utils/cpa_caliber.py`(CPA 派生器共用同一实现,
防「同一客户收入两份结果」)。用独立连接执行,避免表不存在/方言差异污染调用方 session 事务。
2026-10-04 P0 口径纠错(任务 finance-20261004T0905-cpa-revenue-caliber-p0):
① 增列**单据类型白名单** `bill_type IN (1,4,14)`(销售单/退货/换货)——非销售单据
(进货/验收/其它入库/预付款/收款/费用)的 `unit_id` 是**供应商**,混入会把
「客户集中度」度量成「往来单位集中度」(旧口径 Top1/Top2 恰是最大供应商);
② 取舍口径统一为 `(sum_money + sum_tax_money) > 0`(原 `money_only` 与之在本库
当前数据上**逐行同集、差集 0 行**,故行为**零变化**;原「17.2% 低估」表述
经财务复验证否,见 `cpa_caliber` 模块 docstring)。
预警阈值 50% 在新口径下仍不触发(Top1 22.52% < 50%)⇒ 阈值与分级不漂移。
"""
from datetime import date, timedelta
from app.database import get_engine
@@ -831,7 +839,7 @@ def _customer_revenue_shares(db: Session, entity_id: int, lookback_months: int =
end = date.today()
start = end - timedelta(days=max(1, int(lookback_months)) * 30)
shares, total, rng = cpa_caliber.fetch_revenue_rows(
get_engine(), entity_id, start, end, top_n, sign_basis="money_only")
get_engine(), entity_id, start, end, top_n)
if not shares:
return ([], 0.0, None)
return (shares, total, rng)