"""应收账龄口径单一事实源(`hanke_ar_aging`) 财务已定死的口径纪律(ERRATA-02 §五 / DQ 台账 DQ-AR-03): 1. **三口径分列,禁跨口径合计**:`全户口径` / `借方应收户口径` / `贷方户单列(净额)`。 字面值逐字匹配——`贷方户单列(净额)` 写成 `贷方户单列` 会漏 42 行。 2. **对外「超期金额」只取 `借方应收户口径`**;贷方户单列不进分母,任何 `caliber=all` 请求 400。 3. **六档互斥**:0-30 / 31-60 / 61-90 / 90-120 / 120-150 / 150+,逐行六档和 ≡ `total_amt`。 4. `opening_amt`(期初)**只展示**,禁作账龄基数、禁进任何分子分母。 5. 负值保留:禁 `ABS()` / `COALESCE(...,0)` 抹平(DQ-AR-04 杨创 `b_0_30=-20`)。 """ from typing import Optional, Tuple # 表名(DDL:dama/_reports/prodwrite-cma-hanke-ar-aging-20261020/mirror/03_ddl.sql) AR_AGING_TABLE = "hanke_ar_aging" # assignee 派生依赖的只读维度/事实表(§6.2)。三者均为**只读 SELECT**,不写不建。 ZXT_MASTERBILL_TABLE = "zxt_masterbill" # 智享通单据流(unit_id / emp_id / bill_date / abst) EMPLOYEE_DICT_TABLE = "employee_dict" # 员工字典(id → u_Name);当前库内**未落库** UNITS_DICT_TABLE = "units_dict" # 往来单位字典(unit_code → unit_id);当前库内**未落库** # 三口径字面值(逐字匹配,勿改写) CALIBER_FULL = "全户口径" CALIBER_DEBIT = "借方应收户口径" CALIBER_CREDIT_SEPARATE = "贷方户单列(净额)" CALIBERS: Tuple[str, ...] = (CALIBER_FULL, CALIBER_DEBIT, CALIBER_CREDIT_SEPARATE) # 对外超期口径/看板默认口径 DEFAULT_CALIBER = CALIBER_DEBIT # 跨口径合计请求字面值 —— 一律 400 CALIBER_ALL = "all" # 六档列(顺序即展示顺序):(列名, 档位标签, 档位下界天数) BUCKETS: Tuple[Tuple[str, str, int], ...] = ( ("b_0_30", "0-30", 0), ("b_31_60", "31-60", 31), ("b_61_90", "61-90", 61), ("b_90_120", "90-120", 91), ("b_120_150", "120-150", 121), ("b_150p", "150+", 151), ) BUCKET_COLUMNS = tuple(b[0] for b in BUCKETS) # 逾期档位(>30 天)列;最老档下界天数即「逾期 N 天以上」的 N OVERDUE_BUCKETS: Tuple[Tuple[str, int], ...] = tuple((b[0], b[2]) for b in BUCKETS if b[2] > 0) # 90 天以上(含 90-120 档) NINETY_PLUS_COLUMNS = ("b_90_120", "b_120_150", "b_150p") class CaliberError(ValueError): """口径入参非法(未知值 / 跨口径合计)。""" def validate_caliber(caliber: Optional[str], *, allow_none: bool = False) -> str: """校验口径字面值;`all` 与未知值一律拒绝(禁跨口径合计)。""" if caliber is None or caliber == "": if allow_none: return DEFAULT_CALIBER raise CaliberError("caliber 不能为空") if caliber == CALIBER_ALL: raise CaliberError("禁止跨口径合计(caliber=all 不提供):三口径必须分列查询") if caliber not in CALIBERS: raise CaliberError( "未知口径 %r;仅接受字面值:%s" % (caliber, " / ".join(CALIBERS)) ) return caliber def is_credit_separate(caliber: str) -> bool: return caliber == CALIBER_CREDIT_SEPARATE