feat: P0/P1/P2全部功能 — 四层泳道/视角切换/KPI看板/预警/差异反打/预算/知识面板/回顾会/情景预测/Excel导入/角色权限
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"""预算管理模块测试 — 预算计划CRUD + 自动分解 + 版本"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi
|
||||
|
||||
|
||||
class TestBudgetPlans:
|
||||
"""预算计划CRUD测试"""
|
||||
|
||||
BASE = "/api/cma/budget"
|
||||
|
||||
def test_list_plans_empty(self, client: TestClient, db: Session):
|
||||
"""空列表"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
|
||||
resp = client.get(f"{self.BASE}/plans", headers=auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"] == []
|
||||
|
||||
def test_create_plan(self, client: TestClient, db: Session):
|
||||
"""创建预算计划"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_KPI_01")
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 50000.0},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "预算已创建"
|
||||
assert resp.json()["id"] > 0
|
||||
|
||||
def test_create_plan_duplicate_upsert(self, client: TestClient, db: Session):
|
||||
"""重复创建同一KPI+期间 → 更新而非新增"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_KPI_02")
|
||||
|
||||
# 创建
|
||||
client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 30000.0},
|
||||
)
|
||||
# 再次创建(更新)
|
||||
resp = client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 35000.0},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "预算已更新"
|
||||
|
||||
def test_create_plan_missing_fields(self, client: TestClient, db: Session):
|
||||
"""缺少必要参数被拒绝"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": 1}, # 缺少 period 和 budget_value
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_update_plan(self, client: TestClient, db: Session):
|
||||
"""更新预算计划"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_KPI_03")
|
||||
|
||||
create_resp = client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 100.0},
|
||||
)
|
||||
plan_id = create_resp.json()["id"]
|
||||
|
||||
resp = client.put(
|
||||
f"{self.BASE}/plans/{plan_id}",
|
||||
headers=auth_header(token),
|
||||
json={"budget_value": 200.0, "remark": "已更新"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_delete_plan(self, client: TestClient, db: Session):
|
||||
"""删除预算计划"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_KPI_04")
|
||||
|
||||
create_resp = client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 100.0},
|
||||
)
|
||||
plan_id = create_resp.json()["id"]
|
||||
|
||||
resp = client.delete(f"{self.BASE}/plans/{plan_id}", headers=auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["message"] == "预算已删除"
|
||||
|
||||
# 验证已删除
|
||||
get_resp = client.get(f"{self.BASE}/plans", headers=auth_header(token))
|
||||
ids = [p["id"] for p in get_resp.json()["data"]]
|
||||
assert plan_id not in ids
|
||||
|
||||
|
||||
class TestBudgetAutoDecompose:
|
||||
"""预算自动分解测试"""
|
||||
|
||||
BASE = "/api/cma/budget"
|
||||
|
||||
def test_auto_decompose_single_kpi(self, client: TestClient, db: Session):
|
||||
"""单KPI自动分解年度预算为月度(均分)"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_DECOMPOSE")
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/auto-decompose",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "annual_budget": 120000, "year": 2026, "method": "equal"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "已分解" in data["message"]
|
||||
assert len(data["results"]) == 1
|
||||
# 年度预算120000,12个月均分,每月10000
|
||||
monthly = data["results"][0]["monthly"]
|
||||
assert len(monthly) == 12
|
||||
assert monthly[0]["value"] == 10000.0
|
||||
|
||||
def test_auto_decompose_missing(self, client: TestClient, db: Session):
|
||||
"""没有年度预算数据时尝试分解 → 400"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
create_test_kpi(db, kpi_code="BUDGET_NO_DATA")
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/auto-decompose",
|
||||
headers=auth_header(token),
|
||||
json={"year": 2026, "method": "equal"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestBudgetVersions:
|
||||
"""预算版本管理测试"""
|
||||
|
||||
BASE = "/api/cma/budget"
|
||||
|
||||
def test_create_and_submit_version(self, client: TestClient, db: Session):
|
||||
"""创建预算后查询版本并提交"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_VER_KPI")
|
||||
|
||||
# 创建一条预算
|
||||
client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 10000.0},
|
||||
)
|
||||
|
||||
# 查询版本
|
||||
ver_resp = client.get(f"{self.BASE}/versions", headers=auth_header(token))
|
||||
assert ver_resp.status_code == 200
|
||||
assert len(ver_resp.json()["data"]) >= 1
|
||||
|
||||
# 提交版本
|
||||
submit_resp = client.post(
|
||||
f"{self.BASE}/versions/submit",
|
||||
headers=auth_header(token),
|
||||
json={"version": "v1.0"},
|
||||
)
|
||||
assert submit_resp.status_code == 200
|
||||
assert "已提交审批" in submit_resp.json()["message"]
|
||||
|
||||
def test_approve_version(self, client: TestClient, db: Session):
|
||||
"""审批通过版本"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/versions/approve",
|
||||
headers=auth_header(token),
|
||||
json={"version": "v1.0", "action": "approved"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "已批准" in resp.json()["message"]
|
||||
|
||||
def test_reject_version(self, client: TestClient, db: Session):
|
||||
"""驳回版本"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/versions/approve",
|
||||
headers=auth_header(token),
|
||||
json={"version": "v2.0", "action": "rejected"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "已驳回" in resp.json()["message"]
|
||||
Reference in New Issue
Block a user