refactor(backend): 仓储层散参数收敛为查询 DTO
仓储层的 list_*/count_* 此前以 keyword/status/offset/limit 四五个散参数 传参,调用点重复铺开、易漏传且不可扩展;按项目「参数 >4 封装 DTO」的 约定统一收敛为各模块的 Query 对象(如 RoleQuery、BusinessQuery), 服务层与测试调用同步调整,行为不变。 - repositories 层查询方法签名改为 (session, query),分页与筛选条件 由 Query 承载,_apply_filters 同步收口 - schemas 层补齐各模块 Query 模型(approval/business_role/report 等) - services 层调用点同步改为传 Query - tests 随之调整;新增用例:待办角色流转(财务通过后领导出现待办)、 参数预览顺序稳定 验证:全量 pytest 900 passed(11:08);单元 526 passed;ruff check 绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
c48d24ab6d
commit
5ece9f6dab
45 files changed
+1045
-1028
No files matched your search
@@ -205,6 +205,7 @@ uv run pytest tests/integration -q # 仅集成
|
||||
| 2026-10-20 | 全量回归 | 862 | ✅ 862 passed(11:32) | **工作台/报表修复提交后的全量复核**;e2e 三脚本复跑:role_home 4/4、report 12/12、calc_settlement 8/8;calc 审批开关已还原为 1,无 `e2e_role_*` 残留 |
|
||||
| 2026-10-20 | code-review 遗留 2 项 | 全量 875 | ✅ 875 passed(12:13) | **方案版本化 + 审批内容快照**(V11 迁移):审批单 `version_no`/`snapshot_json`,方案行 `is_active` 版本化;编辑/驳回不再物理删方案;历史单无快照时 live 回退并显式标注。新增集成 7 + 单元 6;定点审批/月结/核算/业务/报表全绿;ruff 与 type-check/build 绿 |
|
||||
| 2026-10-20 | 遗留项自评审收口 | 全量 891 | ✅ 891 passed(10:23) | **列表 defer 快照列**(审批列表/工作台待办/超时预警)+ **送审内容未变不重提、方案未变不新增版本**(两个 version_no 语义解耦)+ **月结提交加行锁**。新增单元 10 + 集成 5;单元 525、集成定点 96 全绿 |
|
||||
| 2026-10-22 | 清理批次(散参数 DTO 化) | 全量 900 | ✅ 900 passed(11:08) | **仓储层散参数收敛为查询 DTO**(`list_*(session, query)` 取代 keyword/status/offset/limit 四参数),同步调整服务层与测试调用;单元 526 绿、ruff 绿、前端 type-check 与 build 绿 |
|
||||
| 2026-10-19 | 单元全量 + 报表定点 | 484 | ✅ 484 passed(45.94s);报表单元 11 例 0.70s | **M4 报表模块收尾**(前端四页/打印视图/导出与单元测试补齐)。ruff 绿;前端 `pnpm type-check` 绿。报表集成 7 例记录见上节 |
|
||||
| 2026-10-19 | 全部 | 806 | ✅ 806 passed(7:59) | **M4 ⑤ 报表统计全量回归**(报表新增 18 例:集成 7 + 单元 11;另含并行会话并入的 M3 前端修复与字体合规)。ruff 绿。e2e:`calc_settlement_flow` 8 步由并行会话跑通;`report_flow.py` 待 `E2E_ADMIN_PASSWORD` 运行 |
|
||||
|
||||
|
||||
@@ -451,7 +451,9 @@ class TestContentSnapshot:
|
||||
row.snapshot_json = None
|
||||
session.commit()
|
||||
|
||||
detail = client.get(f"{_APPROVALS}/{order['id']}", headers=finance["headers"]).json()["data"]
|
||||
detail = client.get(f"{_APPROVALS}/{order['id']}", headers=finance["headers"]).json()[
|
||||
"data"
|
||||
]
|
||||
assert detail["content_source"] == "live"
|
||||
assert detail["snapshot"]["captured_at"] is None
|
||||
assert detail["snapshot"]["data"]["business_no"] == data["business_no"]
|
||||
@@ -474,7 +476,9 @@ class TestContentSnapshot:
|
||||
row.ref_id = 999_999_999
|
||||
session.commit()
|
||||
|
||||
detail = client.get(f"{_APPROVALS}/{order['id']}", headers=finance["headers"]).json()["data"]
|
||||
detail = client.get(f"{_APPROVALS}/{order['id']}", headers=finance["headers"]).json()[
|
||||
"data"
|
||||
]
|
||||
assert detail["content_source"] == "missing"
|
||||
assert detail["snapshot"] is None
|
||||
|
||||
@@ -679,9 +683,7 @@ class TestAllocationVersions:
|
||||
data = _create_with_plan(client, admin["headers"], p["id"])
|
||||
|
||||
detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"]
|
||||
assert [(h["version_no"], h["is_active"]) for h in detail["allocation_history"]] == [
|
||||
(1, 1)
|
||||
]
|
||||
assert [(h["version_no"], h["is_active"]) for h in detail["allocation_history"]] == [(1, 1)]
|
||||
|
||||
payload = business_payload(
|
||||
detail["customer_id"],
|
||||
@@ -691,7 +693,9 @@ class TestAllocationVersions:
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"]
|
||||
history = [(h["version_no"], h["is_active"], h["ratio"]) for h in detail["allocation_history"]]
|
||||
history = [
|
||||
(h["version_no"], h["is_active"], h["ratio"]) for h in detail["allocation_history"]
|
||||
]
|
||||
assert history == [(1, 0, "50.00"), (2, 1, "80.00")]
|
||||
assert [a["ratio"] for a in detail["secondary_allocations"]] == ["80.00"]
|
||||
|
||||
|
||||
@@ -13,21 +13,24 @@ audit_service.py「上下文内不得吞掉异常后正常退出」的约定相
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.core.constants import ACTION_CREATE, MODULE_BUSINESS
|
||||
from app.core.constants import ACTION_CREATE, MODULE_BUSINESS, MODULE_RECEIPT
|
||||
from app.core.db import get_session_factory
|
||||
from app.main import app
|
||||
from app.models import OperationLog
|
||||
from app.services import business_service
|
||||
from app.services import business_service, receipt_service
|
||||
from tests.integration.factories import create_business
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
_BUSINESSES = "/api/v1/business/businesses"
|
||||
_CUSTOMERS = "/api/v1/base/customers"
|
||||
_RECEIPTS = "/api/v1/business/receipts"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -160,3 +163,94 @@ class TestAuditRetry:
|
||||
# 每层各写一条记录;修复后重试有上限,最终失败仅一条失败日志
|
||||
assert _log_count(normal["username"], success=False) == base_fail + 1
|
||||
assert _log_count(normal["username"], success=True) == base_ok
|
||||
|
||||
|
||||
def _receipt_log_count(username: str, *, success: bool) -> int:
|
||||
"""按操作人统计「新增回款」操作日志条数。"""
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
stmt = (
|
||||
select(func.count())
|
||||
.select_from(OperationLog)
|
||||
.where(
|
||||
OperationLog.username == username,
|
||||
OperationLog.module == MODULE_RECEIPT,
|
||||
OperationLog.action == ACTION_CREATE,
|
||||
OperationLog.summary.like("新增回款%"),
|
||||
OperationLog.success == int(success),
|
||||
)
|
||||
)
|
||||
return int(session.execute(stmt).scalar_one())
|
||||
|
||||
|
||||
def _receipt_payload(business_id: int) -> dict:
|
||||
return {
|
||||
"business_id": business_id,
|
||||
"receipt_date": str(date.today()),
|
||||
"amount": "1200.00",
|
||||
}
|
||||
|
||||
|
||||
class TestReceiptAuditRetry:
|
||||
"""回款新增的 1062 重试路径(回款与业务同构,但走独立实现)。
|
||||
|
||||
回款侧的重试循环本次从 create 函数体内移入 helper 调用,重试边界、
|
||||
审计条数、批次号重取都必须保持原样 —— 用例会盯住这三条。
|
||||
"""
|
||||
|
||||
def test_首次撞号重试成功_审计恰一条真成功(
|
||||
self, quiet_client: TestClient, normal: dict, monkeypatch
|
||||
) -> None:
|
||||
client = quiet_client
|
||||
business = create_business(client, normal["headers"])
|
||||
first = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert first.status_code == 200, first.text
|
||||
existing_no = first.json()["data"]["receipt_batch_no"]
|
||||
|
||||
real_generate = receipt_service._generate_no
|
||||
calls = {"n": 0}
|
||||
|
||||
def flaky_generate(session):
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
return existing_no # 首次返回库内已存在批次号 → flush 撞 1062
|
||||
return real_generate(session)
|
||||
|
||||
monkeypatch.setattr(receipt_service, "_generate_no", flaky_generate)
|
||||
base_ok = _receipt_log_count(normal["username"], success=True)
|
||||
base_fail = _receipt_log_count(normal["username"], success=False)
|
||||
|
||||
second = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert second.status_code == 200, second.text
|
||||
assert calls["n"] == 2, "重试应恰好再取一次号"
|
||||
assert _receipt_log_count(normal["username"], success=True) == base_ok + 1
|
||||
assert _receipt_log_count(normal["username"], success=False) == base_fail
|
||||
|
||||
def test_持续冲突_最终失败且审计恰一条失败(
|
||||
self, quiet_client: TestClient, normal: dict, monkeypatch
|
||||
) -> None:
|
||||
client = quiet_client
|
||||
business = create_business(client, normal["headers"])
|
||||
first = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert first.status_code == 200, first.text
|
||||
existing_no = first.json()["data"]["receipt_batch_no"]
|
||||
|
||||
def always_conflict(session):
|
||||
return existing_no
|
||||
|
||||
monkeypatch.setattr(receipt_service, "_generate_no", always_conflict)
|
||||
base_ok = _receipt_log_count(normal["username"], success=True)
|
||||
base_fail = _receipt_log_count(normal["username"], success=False)
|
||||
|
||||
second = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert second.status_code == 400, second.text
|
||||
assert _receipt_log_count(normal["username"], success=False) == base_fail + 1
|
||||
assert _receipt_log_count(normal["username"], success=True) == base_ok
|
||||
@@ -452,9 +452,7 @@ class TestEdit:
|
||||
assert resp.status_code == 400, resp.text
|
||||
assert "含合同负责营销人员" in resp.json()["message"]
|
||||
|
||||
def test_二次分配方案合法比例_保存成功且回显(
|
||||
self, client: TestClient, normal: dict
|
||||
) -> None:
|
||||
def test_二次分配方案合法比例_保存成功且回显(self, client: TestClient, normal: dict) -> None:
|
||||
headers = normal["headers"]
|
||||
customer = _customer(client, headers)
|
||||
owner = _personnel(client, headers)
|
||||
|
||||
@@ -138,9 +138,7 @@ def test_总览_绩效合计与台账一致且非法月份400(client: TestClient
|
||||
expected = Decimal(
|
||||
str(
|
||||
session.execute(
|
||||
select(
|
||||
func.coalesce(func.sum(PerformanceLedger.performance_amount), 0)
|
||||
).where(
|
||||
select(func.coalesce(func.sum(PerformanceLedger.performance_amount), 0)).where(
|
||||
PerformanceLedger.settlement_month == _month(),
|
||||
PerformanceLedger.calc_type == 1,
|
||||
PerformanceLedger.is_active == 1,
|
||||
@@ -166,16 +164,12 @@ def test_待办_待审批与未指定批次与同步降级(client: TestClient, a
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
settlement_id = resp.json()["data"]["id"]
|
||||
resp = client.post(
|
||||
f"{_SETTLEMENTS}/{settlement_id}/submit", headers=admin["headers"]
|
||||
)
|
||||
resp = client.post(f"{_SETTLEMENTS}/{settlement_id}/submit", headers=admin["headers"])
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
data = _dashboard(client, admin["headers"])
|
||||
assert data["todos"]["pending_approval_total"] >= 1
|
||||
assert any(
|
||||
item["order_type"] == 2 for item in data["todos"]["pending_approvals"]
|
||||
)
|
||||
assert any(item["order_type"] == 2 for item in data["todos"]["pending_approvals"])
|
||||
assert data["todos"]["unassigned_receipt_total"] >= 1
|
||||
assert data["todos"]["unassigned_receipts"][0]["receipt_id"] == ctx["receipt"]["id"]
|
||||
# ⑥ 未建:显式降级
|
||||
@@ -335,17 +329,17 @@ def test_待办_按角色仅显示可操作节点(
|
||||
assert _dashboard(client, both["headers"])["todos"]["pending_approval_total"] == 2
|
||||
|
||||
finance_orders = {
|
||||
item["order_no"] for item in _dashboard(client, finance["headers"])["todos"]["pending_approvals"]
|
||||
item["order_no"]
|
||||
for item in _dashboard(client, finance["headers"])["todos"]["pending_approvals"]
|
||||
}
|
||||
leader_orders = {
|
||||
item["order_no"] for item in _dashboard(client, leader["headers"])["todos"]["pending_approvals"]
|
||||
item["order_no"]
|
||||
for item in _dashboard(client, leader["headers"])["todos"]["pending_approvals"]
|
||||
}
|
||||
assert finance_orders.isdisjoint(leader_orders)
|
||||
|
||||
|
||||
def test_待办_财务通过后领导出现待办(
|
||||
client: TestClient, admin: dict, finance: dict, leader: dict
|
||||
):
|
||||
def test_待办_财务通过后领导出现待办(client: TestClient, admin: dict, finance: dict, leader: dict):
|
||||
"""真实流转:财务节点通过后,条目从财务待办消失并进入领导待办。"""
|
||||
_chain(client, admin["headers"])
|
||||
resp = client.post(
|
||||
@@ -353,9 +347,7 @@ def test_待办_财务通过后领导出现待办(
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
settlement_id = resp.json()["data"]["id"]
|
||||
resp = client.post(
|
||||
f"{_SETTLEMENTS}/{settlement_id}/submit", headers=admin["headers"]
|
||||
)
|
||||
resp = client.post(f"{_SETTLEMENTS}/{settlement_id}/submit", headers=admin["headers"])
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
order = _dashboard(client, admin["headers"])["todos"]["pending_approvals"][0]
|
||||
|
||||
@@ -326,6 +326,38 @@ class TestPreview:
|
||||
# Fraction 自动约分:4/1000 存储为 1/250,按数值断言
|
||||
assert Fraction(data["rate"]) == Fraction(4, 1000)
|
||||
|
||||
def test_预览参数使用顺序稳定(self, client: TestClient, admin: dict) -> None:
|
||||
"""params_used 的顺序即前端「用了哪些参数」的展示顺序。
|
||||
|
||||
预览实现拆成多段后,各段的调用先后决定参数读取先后;顺序漂移会让
|
||||
复核者看到的参数清单与核算口径对不上(不报错,但会误导判读)。
|
||||
"""
|
||||
payload = {
|
||||
"business_role": "salesperson",
|
||||
"customer_type": "personal",
|
||||
"product_category": "municipal_water",
|
||||
"ship_date": "2025-12-15",
|
||||
"receipt_date": "2026-01-10",
|
||||
"revenue": "100000",
|
||||
"profit": "45000",
|
||||
"received_amount": "60000",
|
||||
"barter": True,
|
||||
"probation": True,
|
||||
}
|
||||
data = client.post(f"{_PARAMS}/preview", json=payload, headers=admin["headers"]).json()[
|
||||
"data"
|
||||
]
|
||||
assert [p["param_code"] for p in data["params_used"]] == [
|
||||
"salesperson.rate.start",
|
||||
"salesperson.rate.slope",
|
||||
"salesperson.rate.floor",
|
||||
"salesperson.days.boundary",
|
||||
"product.threshold",
|
||||
"barter.discount",
|
||||
"payment.start.ratio",
|
||||
"probation.annual.threshold",
|
||||
]
|
||||
|
||||
def test_经理聚合口径_预览拒绝(self, client: TestClient, admin: dict) -> None:
|
||||
payload = {
|
||||
"business_role": "sales_manager",
|
||||
|
||||
@@ -44,7 +44,9 @@ def _role_id(code: str) -> int:
|
||||
)
|
||||
|
||||
|
||||
def _person(client: TestClient, headers: dict, *, role: str = "salesperson", manager_id=None) -> dict:
|
||||
def _person(
|
||||
client: TestClient, headers: dict, *, role: str = "salesperson", manager_id=None
|
||||
) -> dict:
|
||||
body = {
|
||||
"personnel_name": f"t_报表_{uuid.uuid4().hex[:8]}",
|
||||
"business_role_id": _role_id(role),
|
||||
@@ -194,9 +196,7 @@ def test_部门汇总_无成员经理出零值行(admin, client: TestClient):
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
team = next(
|
||||
(t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"]), None
|
||||
)
|
||||
team = next((t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"]), None)
|
||||
assert team is not None
|
||||
assert Decimal(str(team["team_base"])) == Decimal("0")
|
||||
assert team["member_count"] == 0
|
||||
@@ -224,7 +224,14 @@ def test_回款统计_空桶恒在且分摊拆行金额只计一次(admin, clien
|
||||
|
||||
assert len(data["buckets"]) == 8
|
||||
assert [b["code"] for b in data["buckets"]] == [
|
||||
"le0", "1-30", "31-60", "61-90", "91-180", "181-360", "361+", "none",
|
||||
"le0",
|
||||
"1-30",
|
||||
"31-60",
|
||||
"61-90",
|
||||
"91-180",
|
||||
"181-360",
|
||||
"361+",
|
||||
"none",
|
||||
]
|
||||
# 60 天前发货 → 落在 61-90 桶(发货日首日计第 1 天)
|
||||
bucket = next(b for b in data["buckets"] if b["code"] == "61-90")
|
||||
@@ -293,11 +300,15 @@ def test_导出_excel含签字栏且入操作日志(admin, client: TestClient):
|
||||
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
log = session.execute(
|
||||
select(OperationLog)
|
||||
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
|
||||
.order_by(OperationLog.id.desc())
|
||||
).scalars().first()
|
||||
log = (
|
||||
session.execute(
|
||||
select(OperationLog)
|
||||
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
|
||||
.order_by(OperationLog.id.desc())
|
||||
)
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
assert log is not None
|
||||
|
||||
|
||||
@@ -389,19 +400,21 @@ def _latest_export_log():
|
||||
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
return session.execute(
|
||||
select(OperationLog)
|
||||
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
|
||||
.order_by(OperationLog.id.desc())
|
||||
).scalars().first()
|
||||
return (
|
||||
session.execute(
|
||||
select(OperationLog)
|
||||
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
|
||||
.order_by(OperationLog.id.desc())
|
||||
)
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def test_部门汇总_无台账经理挂上级不报500(admin, client: TestClient):
|
||||
"""经理 A 无台账且上级是经理 B:不能因 member_map 直接索引而 KeyError。"""
|
||||
manager_b = _person(client, admin["headers"], role="sales_manager")
|
||||
manager_a = _person(
|
||||
client, admin["headers"], role="sales_manager", manager_id=manager_b["id"]
|
||||
)
|
||||
manager_a = _person(client, admin["headers"], role="sales_manager", manager_id=manager_b["id"])
|
||||
|
||||
resp = client.get(
|
||||
f"{_REPORTS}/department-summary",
|
||||
@@ -467,9 +480,7 @@ def test_部门汇总_经理个人户计入时基数等于成员合计(admin, cl
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
team = next(
|
||||
t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"]
|
||||
)
|
||||
team = next(t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"])
|
||||
assert Decimal(str(team["team_base"])) == Decimal("100.00")
|
||||
assert sum(
|
||||
(Decimal(str(m["performance_sum"])) for m in team["members"]), Decimal("0")
|
||||
@@ -516,9 +527,7 @@ def test_部门汇总_按经理筛选不误判其他团队下属(admin, client:
|
||||
assert [t["manager_id"] for t in data["teams"]] == [manager_1["id"]]
|
||||
assert data["unassigned"] == []
|
||||
assert all(
|
||||
m["personnel_id"] != salesperson_2["id"]
|
||||
for team in data["teams"]
|
||||
for m in team["members"]
|
||||
m["personnel_id"] != salesperson_2["id"] for team in data["teams"] for m in team["members"]
|
||||
)
|
||||
assert Decimal(str(data["reconciliation"]["other_team_base"])) > 0
|
||||
|
||||
@@ -557,9 +566,7 @@ def test_部门汇总_对账恒等式(admin, client: TestClient):
|
||||
)
|
||||
|
||||
|
||||
def _two_manager_scene(
|
||||
client: TestClient, headers: dict
|
||||
) -> tuple[dict, dict, dict, dict, dict]:
|
||||
def _two_manager_scene(client: TestClient, headers: dict) -> tuple[dict, dict, dict, dict, dict]:
|
||||
"""两位经理各有下属与业务 + 一位零成员经理(无聚合行)。"""
|
||||
manager_1 = _person(client, headers, role="sales_manager")
|
||||
manager_2 = _person(client, headers, role="sales_manager")
|
||||
@@ -777,9 +784,7 @@ def test_导出取数异常_失败审计留痕(admin, client: TestClient, monkey
|
||||
assert "导出个人绩效工资明细表" in (log.summary or "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path", ["/personal-detail", "/department-summary", "/payment-stats"]
|
||||
)
|
||||
@pytest.mark.parametrize("path", ["/personal-detail", "/department-summary", "/payment-stats"])
|
||||
def test_非法月份_查询返回400(admin, client: TestClient, path: str):
|
||||
"""非法月份必须是参数错误 400,不能落 500。"""
|
||||
resp = client.get(
|
||||
@@ -790,9 +795,7 @@ def test_非法月份_查询返回400(admin, client: TestClient, path: str):
|
||||
assert resp.status_code == 400, resp.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code", ["personal-detail", "department-summary", "payment-stats"]
|
||||
)
|
||||
@pytest.mark.parametrize("code", ["personal-detail", "department-summary", "payment-stats"])
|
||||
def test_非法月份_导出返回400(admin, client: TestClient, code: str):
|
||||
"""导出手工构造查询模型同样要转 400。"""
|
||||
resp = client.get(
|
||||
|
||||
@@ -127,8 +127,7 @@ class TestExcel模板:
|
||||
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")
|
||||
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"
|
||||
@@ -142,16 +141,14 @@ class TestExcel模板:
|
||||
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))
|
||||
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\")"
|
||||
evil = '=HYPERLINK("http://evil")'
|
||||
content = _build("personal-detail", [_personal_row(business_no=evil)])
|
||||
workbook = load_workbook(io.BytesIO(content))
|
||||
values = [
|
||||
|
||||
Reference in new issue
Block a user