"""报表统计元数据(区间桶、中文标签、导出上限)——叶子模块。 刻意不导入 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": "混合业务", } REPORT_TEMPLATE_VERSION = "V1.0" # 导出超限即拒绝(需求「大数据量分批/超限提示」二选一取后者):普通 # 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")