"""报表辅助纯函数与 Excel 模板构建单元测试(M4 ⑤)。 这组用例守的是「不测会静默出错」的三类点: - 区间分桶边界错一档 → 金额进错桶,但报表仍然「能打开」,没人会发现 - 标签映射漏一个枚举值 → 报表少一类数据或显示英文编码 - 导出模板缺签字栏 / 金额被转 float / 文本公式注入 → 交给财务的签字稿失真 """ import io from datetime import date from decimal import Decimal from openpyxl import load_workbook from app.core import report_meta from app.core.constants import CUSTOMER_TYPES, PRODUCT_CATEGORIES from app.services import report_export class Test区间分桶: """回款天数 → 8 档区间(含 ≤0 与无天数两个兜底桶)。""" def test_边界值全部落入正确区间(self): cases = [ (None, "none"), (-5, "le0"), (0, "le0"), (1, "1-30"), (30, "1-30"), (31, "31-60"), (60, "31-60"), (61, "61-90"), (90, "61-90"), (91, "91-180"), (180, "91-180"), (181, "181-360"), (360, "181-360"), (361, "361+"), (1000, "361+"), ] for days, code in cases: assert report_meta.bucket_of(days).code == code, f"days={days}" def test_八档且编码唯一(self): buckets = report_meta.get_payment_day_buckets() assert len(buckets) == 8 assert len({b.code for b in buckets}) == 8 class Test标签覆盖: """标签键集合必须覆盖业务常量——防常量新增后报表静默少算/显示编码。""" def test_客户类型标签覆盖全部类型(self): assert set(report_meta.CUSTOMER_TYPE_LABELS) >= CUSTOMER_TYPES def test_产品类别标签覆盖常量与混合业务(self): assert set(report_meta.PRODUCT_CATEGORY_LABELS) >= PRODUCT_CATEGORIES assert "mixed" in report_meta.PRODUCT_CATEGORY_LABELS def _build(code: str, rows: list[dict], buckets: dict | None = None) -> bytes: return report_export.build_workbook( code=code, title="测试报表", filters="统计月份 2026-10", rows=rows, summary=None, buckets=buckets, template_version="V1.0", operator_name="admin", company_name="衍水集团", ) def _sheet_text(content: bytes) -> str: workbook = load_workbook(io.BytesIO(content)) return "\n".join( str(cell.value) for row in workbook.active.iter_rows() for cell in row if cell.value is not None ) def _personal_row(**overrides) -> dict: row = { "business_no": "YW001", "ship_date": date(2026, 10, 1), "receipt_date": date(2026, 10, 20), "payment_days": 20, "product_category": "municipal_water", "customer_type": "personal", "role_name": "营销员", "rate_text": "88/9000", "weight_r": 100, "performance_amount": Decimal("39.11"), "payable_amount": Decimal("39.11"), } row.update(overrides) return row class TestExcel模板: """导出稿是交给财务复核与领导签字的,结构缺失等同于没有模板。""" def test_页眉标题签字栏齐全(self): text = _sheet_text(_build("personal-detail", [_personal_row()])) assert "衍水集团" in text assert "测试报表" in text assert "统计月份 2026-10" in text assert "模板版本:V1.0" in text assert "财务复核" in text assert "领导签字" in text def test_空数据不炸且给提示行(self): text = _sheet_text(_build("personal-detail", [])) assert "本筛选条件无数据" in text assert "财务复核" in text def test_金额为数值单元格且两位格式(self): # openpyxl 写入 Decimal 后读回为 int/float(Excel 单元格本身是数值), # 这里断言「是数值而非文本」+ 显示格式;计算精度在服务层 Decimal 侧保证 content = _build("personal-detail", [_personal_row()]) workbook = load_workbook(io.BytesIO(content)) sheet = workbook.active amount_cells = [ cell for row in sheet.iter_rows() for cell in row if isinstance(cell.value, (int, float)) and Decimal(str(cell.value)) == Decimal("39.11") ] assert amount_cells, "未找到金额数值单元格(Decimal 被转成了文本?)" assert amount_cells[0].number_format == "#,##0.00" def test_合计行等于明细之和且标签只出现一次(self): rows = [ _personal_row(performance_amount=Decimal("10.00"), payable_amount=Decimal("10.00")), _personal_row(performance_amount=Decimal("5.50"), payable_amount=Decimal("5.50")), ] content = _build("personal-detail", rows) workbook = load_workbook(io.BytesIO(content)) cells = [cell for row in workbook.active.iter_rows() for cell in row] values = [ Decimal(str(cell.value)) for cell in cells if isinstance(cell.value, (int, float)) ] assert Decimal("15.50") in values # 「合计」标签只写一次(写满每个文本列是回归缺陷) assert sum(1 for cell in cells if cell.value == "合计") == 1 def test_文本公式注入被前置单引号(self): evil = "=HYPERLINK(\"http://evil\")" content = _build("personal-detail", [_personal_row(business_no=evil)]) workbook = load_workbook(io.BytesIO(content)) values = [ cell.value for row in workbook.active.iter_rows() for cell in row if isinstance(cell.value, str) ] assert any(value.startswith("'=") for value in values), "公式注入未转义" def test_回款统计附两个维度对比sheet(self): buckets = { "by_customer_type": [ { "label": "个人户", "receipt_amount": Decimal("100.00"), "performance_amount": Decimal("1.00"), "payable_amount": Decimal("1.00"), "ledger_count": 1, } ], "by_product_category": [], } content = _build( "payment-stats", [ { "label": "1-30 天", "receipt_amount": Decimal("100.00"), "receipt_count": 1, "performance_amount": Decimal("1.00"), "payable_amount": Decimal("1.00"), "ledger_count": 1, } ], buckets=buckets, ) workbook = load_workbook(io.BytesIO(content)) assert workbook.sheetnames == ["回款统计", "客户类型对比", "产品类别对比"] def test_业务统计状态转中文(self): rows = [ { "business_no": "YW001", "business_date": date(2026, 10, 1), "customer_name": "测试客户", "owner_personnel_name": "张三", "status": 2, "total_sale_amount": Decimal("100.00"), "total_ship_amount": Decimal("100.00"), "total_received_amount": Decimal("100.00"), "performance_amount": Decimal("1.00"), "payable_amount": Decimal("1.00"), } ] text = _sheet_text(_build("business-stats", rows)) assert "已完成" in text