2026-10-19 01:53:34 -04:00
|
|
|
|
"""报表统计元数据(区间桶、中文标签、导出上限)——叶子模块。
|
|
|
|
|
|
|
|
|
|
|
|
刻意不导入 app 内任何模块:report_service 与 report_export 都要消费这里的
|
|
|
|
|
|
定义,若它反向依赖 service 层会形成循环导入;也不走参数体系——FR-031
|
|
|
|
|
|
「区间可配置」一期以代码常量落地(参数化需注册表 + 迁移 + init-dev +
|
|
|
|
|
|
registry 测试四处同改,业务未提诉求前不做),配置缝收在
|
|
|
|
|
|
get_payment_day_buckets 单函数,未来在线可配时只改该函数。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
|
|
|
|
class DayBucket:
|
|
|
|
|
|
"""回款天数区间档位:min/max 均为 None 表示「无回款天数」兜底桶。"""
|
|
|
|
|
|
|
|
|
|
|
|
code: str
|
|
|
|
|
|
label: str
|
|
|
|
|
|
min_days: int | None
|
|
|
|
|
|
max_days: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 8 档区间(催办清单 R2 默认口径)。两个兜底桶必须保留:payment_days 可空
|
|
|
|
|
|
# (回款未指定发货批次)且可为负(先款后货照实负值),缺了它们这些行会
|
|
|
|
|
|
# 静默消失、金额对不上。
|
|
|
|
|
|
PAYMENT_DAY_BUCKETS: tuple[DayBucket, ...] = (
|
|
|
|
|
|
DayBucket("le0", "≤0 天(先款后货)", None, 0),
|
|
|
|
|
|
DayBucket("1-30", "1-30 天", 1, 30),
|
|
|
|
|
|
DayBucket("31-60", "31-60 天", 31, 60),
|
|
|
|
|
|
DayBucket("61-90", "61-90 天", 61, 90),
|
|
|
|
|
|
DayBucket("91-180", "91-180 天", 91, 180),
|
|
|
|
|
|
DayBucket("181-360", "181-360 天", 181, 360),
|
|
|
|
|
|
DayBucket("361+", "361 天以上", 361, None),
|
|
|
|
|
|
DayBucket("none", "无回款天数", None, None),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
CUSTOMER_TYPE_LABELS: dict[str, str] = {
|
|
|
|
|
|
"personal": "个人户",
|
|
|
|
|
|
"manager_personal": "经理个人户",
|
|
|
|
|
|
"enterprise": "企业户",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# mixed 不在 core.constants.PRODUCT_CATEGORIES(那只是可维护的档案类别),
|
|
|
|
|
|
# 但台账行可以是 mixed(混合业务行级明细入 snapshot_json),报表必须能标它
|
|
|
|
|
|
PRODUCT_CATEGORY_LABELS: dict[str, str] = {
|
|
|
|
|
|
"municipal_water": "市政水利",
|
|
|
|
|
|
"rail_transit": "轨道交通",
|
|
|
|
|
|
"mixed": "混合业务",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 21:44:43 -04:00
|
|
|
|
# 业务状态中文标签:导出列、筛选页眉与前端标签共用同一份映射
|
|
|
|
|
|
BUSINESS_STATUS_LABELS: dict[int, str] = {1: "进行中", 2: "已完成", 3: "已终止"}
|
|
|
|
|
|
|
|
|
|
|
|
REPORT_TEMPLATE_VERSION = "V1.2"
|
2026-10-19 01:53:34 -04:00
|
|
|
|
# 导出超限即拒绝(需求「大数据量分批/超限提示」二选一取后者):普通
|
|
|
|
|
|
# Workbook 全量载入内存,2 万行约数十 MB,超过该量级必须引导按月导出
|
|
|
|
|
|
REPORT_EXPORT_MAX_ROWS = 20000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_payment_day_buckets() -> tuple[DayBucket, ...]:
|
|
|
|
|
|
"""区间档位唯一出口:前端下拉与分桶都以本函数返回为准,禁止硬编码。
|
|
|
|
|
|
|
|
|
|
|
|
未来「区间在线可配」时改此处读参数即可,调用方与前端零改动。
|
|
|
|
|
|
"""
|
|
|
|
|
|
return PAYMENT_DAY_BUCKETS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bucket_of(payment_days: int | None) -> DayBucket:
|
|
|
|
|
|
"""把单笔回款天数落入对应区间桶;未匹配任何区间按「无回款天数」兜底。"""
|
|
|
|
|
|
if payment_days is None:
|
|
|
|
|
|
return next(b for b in PAYMENT_DAY_BUCKETS if b.code == "none")
|
|
|
|
|
|
for bucket in PAYMENT_DAY_BUCKETS:
|
|
|
|
|
|
if bucket.code == "none":
|
|
|
|
|
|
continue
|
|
|
|
|
|
lower_ok = bucket.min_days is None or payment_days >= bucket.min_days
|
|
|
|
|
|
upper_ok = bucket.max_days is None or payment_days <= bucket.max_days
|
|
|
|
|
|
if lower_ok and upper_ok:
|
|
|
|
|
|
return bucket
|
|
|
|
|
|
return next(b for b in PAYMENT_DAY_BUCKETS if b.code == "none")
|