fix(dashboard): 修复普通用户落地页与待办角色过滤
- 新增 router/access.ts 统一准入与落地页(工作台→业务数据→回款), 守卫/登录回跳/菜单/面包屑复用;普通用户不再被无限重定向 - 待办按角色映射在途审批节点(财务/领导/admin),admin 不代办但可查全部 - 预警排除已终止业务;WarningBlock.total 为全量命中数;回款趋势改日期范围 - 工作台加载请求序号防竞态;KPI 口径说明补经理分成/调整单 - 新增 role_home_flow e2e,更新 system_management 普通用户断言 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
ebb3a92f25
commit
39a3fa7796
16 files changed
+590
-59
No files matched your search
@@ -191,7 +191,7 @@ def test_预警_审批超时命中且参数关闭后不产生(client: TestClient
|
||||
client.post(f"{_SETTLEMENTS}/{settlement_id}/submit", headers=admin["headers"])
|
||||
|
||||
# 把待审批单提交时间改到 4 天前(阈值默认 3 天)
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy import update
|
||||
|
||||
from app.core.db import get_session_factory
|
||||
from app.models import ApprovalOrder
|
||||
@@ -230,3 +230,224 @@ def test_动态_来源操作日志(client: TestClient, admin: dict):
|
||||
data = _dashboard(client, admin["headers"])
|
||||
assert data["activities"], "工作台动态为空:写操作应产生操作日志"
|
||||
assert {"module", "action", "summary", "create_time"} <= set(data["activities"][0])
|
||||
|
||||
|
||||
# ==== 角色待办 / 预警阈值 / 趋势(2026-10-19 code-review 修复回归)====
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def finance(make_user, auth_headers) -> dict:
|
||||
user = make_user(["finance"])
|
||||
user["headers"] = auth_headers(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def leader(make_user, auth_headers) -> dict:
|
||||
user = make_user(["leader"])
|
||||
user["headers"] = auth_headers(user)
|
||||
return user
|
||||
|
||||
|
||||
def _insert_approval(status: int) -> None:
|
||||
"""直插在途审批单(避免依赖两次流转,专注工作台过滤口径)。"""
|
||||
from app.core.db import get_session_factory
|
||||
from app.models import ApprovalOrder
|
||||
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
session.add(
|
||||
ApprovalOrder(
|
||||
order_no=f"AP{uuid.uuid4().hex[:12]}",
|
||||
order_type=2,
|
||||
ref_id=0,
|
||||
refer_no="t_工作台",
|
||||
status=status,
|
||||
submitter_name="t_admin",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _set_param(client: TestClient, headers: dict, code: str, value: str) -> None:
|
||||
resp = client.post(
|
||||
f"{_PARAMS}/changes",
|
||||
json={
|
||||
"change_type": 1,
|
||||
"remark": f"t_工作台:{code}",
|
||||
"items": [{"param_code": code, "dimension_value": "", "value": value}],
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
|
||||
def _shipment_scene(client: TestClient, headers: dict, *, days_ago: int) -> tuple[dict, dict]:
|
||||
"""造一笔无回款的发货(用于回款逾期预警)。"""
|
||||
from tests.integration.factories import business_item, business_payload, customer
|
||||
|
||||
person = _person(client, headers)
|
||||
cust = customer(client, headers)
|
||||
resp = client.post(
|
||||
_BUSINESSES,
|
||||
json=business_payload(
|
||||
cust["id"],
|
||||
owner_personnel_id=person["id"],
|
||||
items=[business_item(cost_amount="3000.00")],
|
||||
),
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
business = resp.json()["data"]
|
||||
ship_date = (date.today() - timedelta(days=days_ago)).isoformat()
|
||||
resp = client.post(
|
||||
_SHIPMENTS,
|
||||
json={
|
||||
"business_id": business["id"],
|
||||
"ship_date": ship_date,
|
||||
"items": [{"product_name": "t_工作台逾期", "quantity": "1", "sale_price": "1.00"}],
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
return business, resp.json()["data"]
|
||||
|
||||
|
||||
def test_待办_按角色仅显示可操作节点(
|
||||
client: TestClient, admin: dict, finance: dict, leader: dict, make_user, auth_headers
|
||||
):
|
||||
"""财务只看到待财务复核、领导只看到待领导审核;admin 与多角色看并集。"""
|
||||
_insert_approval(1)
|
||||
_insert_approval(2)
|
||||
|
||||
assert _dashboard(client, finance["headers"])["todos"]["pending_approval_total"] == 1
|
||||
assert _dashboard(client, leader["headers"])["todos"]["pending_approval_total"] == 1
|
||||
assert _dashboard(client, admin["headers"])["todos"]["pending_approval_total"] == 2
|
||||
|
||||
both = make_user(["finance", "leader"])
|
||||
both["headers"] = auth_headers(both)
|
||||
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"]
|
||||
}
|
||||
leader_orders = {
|
||||
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
|
||||
):
|
||||
"""真实流转:财务节点通过后,条目从财务待办消失并进入领导待办。"""
|
||||
_chain(client, admin["headers"])
|
||||
resp = client.post(
|
||||
f"{_SETTLEMENTS}/generate", json={"month": _month()}, headers=admin["headers"]
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
settlement_id = resp.json()["data"]["id"]
|
||||
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]
|
||||
assert _dashboard(client, finance["headers"])["todos"]["pending_approval_total"] == 1
|
||||
assert _dashboard(client, leader["headers"])["todos"]["pending_approval_total"] == 0
|
||||
|
||||
resp = client.post(
|
||||
f"/api/v1/system/approvals/{order['order_id']}/action",
|
||||
json={"action": 1},
|
||||
headers=finance["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert _dashboard(client, finance["headers"])["todos"]["pending_approval_total"] == 0
|
||||
assert _dashboard(client, leader["headers"])["todos"]["pending_approval_total"] == 1
|
||||
|
||||
|
||||
def test_预警_回款逾期命中且阈值参数生效(client: TestClient, admin: dict):
|
||||
"""发货超阈值且无任何回款 → 命中;调大阈值后不再命中。"""
|
||||
_, shipment = _shipment_scene(client, admin["headers"], days_ago=120)
|
||||
|
||||
data = _dashboard(client, admin["headers"])
|
||||
assert any(
|
||||
item["code"] == "receipt_overdue" and item["object_id"] == shipment["id"]
|
||||
for item in data["warnings"]["items"]
|
||||
), "120 天未回款的发货批次未命中预警"
|
||||
|
||||
_set_param(client, admin["headers"], "warning.receipt.overdue.days", "200")
|
||||
data = _dashboard(client, admin["headers"])
|
||||
assert not any(
|
||||
item["code"] == "receipt_overdue" and item["object_id"] == shipment["id"]
|
||||
for item in data["warnings"]["items"]
|
||||
), "阈值调大后仍命中,说明参数未生效"
|
||||
|
||||
|
||||
def test_预警_终止业务不再产生回款逾期(client: TestClient, admin: dict):
|
||||
"""已终止业务不再催收,其未回款发货单不得留在预警列表。"""
|
||||
business, shipment = _shipment_scene(client, admin["headers"], days_ago=120)
|
||||
resp = client.put(
|
||||
f"{_BUSINESSES}/{business['id']}/status",
|
||||
json={"status": 3, "reason": "t_工作台终止"},
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
data = _dashboard(client, admin["headers"])
|
||||
assert not any(item["object_id"] == shipment["id"] for item in data["warnings"]["items"])
|
||||
assert data["warnings"]["receipt_overdue_total"] == 0
|
||||
|
||||
|
||||
def test_预警_总数不受展示上限截断(client: TestClient, admin: dict, monkeypatch):
|
||||
"""items 只展示前 N 条,徽标 total 必须仍是全量命中数。"""
|
||||
from app.services import dashboard_service
|
||||
|
||||
_shipment_scene(client, admin["headers"], days_ago=120)
|
||||
_shipment_scene(client, admin["headers"], days_ago=120)
|
||||
monkeypatch.setattr(dashboard_service, "_WARNING_LIMIT", 1)
|
||||
|
||||
data = _dashboard(client, admin["headers"])
|
||||
assert data["warnings"]["total"] == 2
|
||||
assert len(data["warnings"]["items"]) == 1
|
||||
assert data["warnings"]["total"] == data["warnings"]["receipt_overdue_total"]
|
||||
|
||||
|
||||
def test_总览_回款趋势按到账月跨月归属(client: TestClient, admin: dict):
|
||||
"""趋势窗口按 receipt_date 范围过滤,月初/月末不得串月。"""
|
||||
from tests.integration.factories import business_item, business_payload, customer
|
||||
|
||||
person = _person(client, admin["headers"])
|
||||
cust = customer(client, admin["headers"])
|
||||
resp = client.post(
|
||||
_BUSINESSES,
|
||||
json=business_payload(
|
||||
cust["id"],
|
||||
owner_personnel_id=person["id"],
|
||||
items=[business_item(cost_amount="3000.00")],
|
||||
),
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
business = resp.json()["data"]
|
||||
|
||||
first_of_month = date.today().replace(day=1)
|
||||
previous_last = first_of_month - timedelta(days=1)
|
||||
for receipt_date in (previous_last, first_of_month):
|
||||
resp = client.post(
|
||||
_RECEIPTS,
|
||||
json={
|
||||
"business_id": business["id"],
|
||||
"receipt_date": receipt_date.isoformat(),
|
||||
"amount": "1000.00",
|
||||
},
|
||||
headers=admin["headers"],
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
trend = {
|
||||
item["month"]: Decimal(str(item["receipt_total"] or 0))
|
||||
for item in _dashboard(client, admin["headers"])["overview"]["trend"]
|
||||
}
|
||||
assert trend[previous_last.strftime("%Y-%m")] == Decimal("1000.00")
|
||||
assert trend[first_of_month.strftime("%Y-%m")] == Decimal("1000.00")
|
||||
Reference in new issue
Block a user