fix(m3): code-review 15 项修复(13 项落地,2 项依赖分摊语义记待办)
- 审批:删 _flush_order 撞键内部重试(rollback 会回掉同事务业务行造成 静默丢数据,异常改由 create 外层重试整体重来/update 路径审计收敛); act() 出参固化前置到 commit 之前(commit 后查询失败会把已通过的审批 记成失败审计);驳回/撤回清空方案生效行(防被否决方案进 M3 快照); keyword 改 contains(autoescape=True) 对齐全库转义口径;撤单抽 withdraw_pending_orders 供编辑清空方案时显式调用 - 业务:删业务遇在途审批单禁删 400、终态审批单随 _hard_delete 清理; 编辑清空方案同步撤回在途单;历史离职参与人编辑放行(冗余姓名固化 语义,仅新增参与人做在职校验) - 迁移:business_secondary_allocations 补 uk(business_id, personnel_id) (模型/迁移 py/导出 SQL/init-dev 四方同步,开发库往返重放验证) - 设置:upload_logo 改同步 def(阻塞操作不卡事件循环)+ file.size 先拦; company_name schema 剥空白防全空白入库;logo 旧文件清理移到 commit 成功后(防库指旧路径而文件已删断链);uploadLogo 走 request() 解包 (原直发 service.post 不解包信封,logoUrl 会被置 undefined) - 备份:restore_drill --drop-exec 先清全部历史 drill 库再演练并即删本轮 (原语义永远删错对象,每轮净增一份整库拷贝) - 测试:conftest 清理补 system_settings 种子行与 uploads/logo 文件; 新增 5 例(驳回清方案/清空撤单/在途禁删终态随删/离职放行/BOOL 边界); 全量回归 701 passed - 遗留 2 项记 AI_CONTEXT(方案版本承载、审批单快照,依赖 ④ 分摊语义) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
fc8a42bcd9
commit
1c93ea0a4b
14 files changed
+338
-79
No files matched your search
@@ -21,6 +21,7 @@
|
||||
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import delete, select, update
|
||||
@@ -48,6 +49,7 @@ from app.models import (
|
||||
Role,
|
||||
Shipment,
|
||||
ShipmentItem,
|
||||
SystemSetting,
|
||||
User,
|
||||
UserRole,
|
||||
)
|
||||
@@ -172,7 +174,7 @@ def _purge_test_data() -> None:
|
||||
# 人员已先删(business_role_id 外键 RESTRICT),直建的测试岗位此时可安全删
|
||||
session.execute(delete(BusinessRole).where(BusinessRole.role_code.like("t\\_%")))
|
||||
|
||||
# ---- M3:审批单 / 二次分配 / 参数开关基线 ----
|
||||
# ---- M3:审批单 / 二次分配 / 参数开关 / 系统设置基线 ----
|
||||
# 审批单无 t_ 前缀可用,按 AP 编号前缀全量清(steps 外键 CASCADE 随删;
|
||||
# ref_id 固化无外键,业务已在上一步删除,无悬挂影响)
|
||||
session.execute(delete(ApprovalOrder).where(ApprovalOrder.order_no.like("AP%")))
|
||||
@@ -183,7 +185,18 @@ def _purge_test_data() -> None:
|
||||
stray_batches = {v.batch_id for v in stray_values}
|
||||
session.execute(delete(ParamValue).where(ParamValue.id > 21))
|
||||
session.execute(delete(ParamChangeBatch).where(ParamChangeBatch.id.in_(stray_batches)))
|
||||
# 系统设置恢复种子单行(test_settings 会改公司名/传 logo——残留会让
|
||||
# 后续 e2e 的品牌断言在污染状态下运行);uploads 下的测试 logo 一并删
|
||||
session.execute(
|
||||
update(SystemSetting)
|
||||
.where(SystemSetting.id == 1)
|
||||
.values(company_name="衍水集团", logo_path=None)
|
||||
)
|
||||
session.commit()
|
||||
logo_dir = Path(__file__).resolve().parents[2] / "uploads" / "logo"
|
||||
if logo_dir.is_dir():
|
||||
for f in logo_dir.iterdir():
|
||||
f.unlink(missing_ok=True)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -261,9 +261,28 @@ class TestSwitchOn:
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_驳回后方案行清空_不留在生效表(
|
||||
self, client: TestClient, admin: dict, finance, switched_on
|
||||
) -> None:
|
||||
"""被否决的方案不得以「当前生效行」身份留在表里(M3 快照会取到)。"""
|
||||
p = _person(client, admin["headers"])
|
||||
data = _create_with_plan(client, admin["headers"], p["id"])
|
||||
order = _latest_order(client, finance["headers"], data["business_no"])
|
||||
|
||||
resp = client.post(
|
||||
f"{_APPROVALS}/{order['id']}/action",
|
||||
json={"action": 2, "opinion": "方案不合理"},
|
||||
headers=finance["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"]
|
||||
assert detail["secondary_allocations"] == [], "被驳回的方案应从生效表清空"
|
||||
assert detail["allocation_hint"] is None
|
||||
|
||||
|
||||
class TestResubmit:
|
||||
"""再编辑撤旧建新。"""
|
||||
"""再编辑撤旧建新 / 清空方案撤在途单。"""
|
||||
|
||||
def test_在途单撤回并新建(
|
||||
self, client: TestClient, admin: dict, finance, leader, switched_on
|
||||
@@ -293,6 +312,64 @@ class TestResubmit:
|
||||
withdraw_steps = [s for s in old_detail["data"]["steps"] if s["action"] == 4]
|
||||
assert withdraw_steps and withdraw_steps[0]["node"] == 0
|
||||
|
||||
def test_编辑清空方案_在途单撤回不再悬空(
|
||||
self, client: TestClient, admin: dict, finance, switched_on
|
||||
) -> None:
|
||||
"""空数组=清空方案:旧在途单必须撤回,否则挂着不存在的方案走审批。"""
|
||||
p = _person(client, admin["headers"])
|
||||
data = _create_with_plan(client, admin["headers"], p["id"])
|
||||
first = _latest_order(client, finance["headers"], data["business_no"])
|
||||
assert first["status"] == 1
|
||||
|
||||
detail = client.get(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"]).json()["data"]
|
||||
payload = business_payload(detail["customer_id"], secondary_allocations=[])
|
||||
resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"])
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
rows = client.get(
|
||||
_APPROVALS,
|
||||
params={"keyword": data["business_no"], "pageSize": 10},
|
||||
headers=finance["headers"],
|
||||
).json()["data"]["list"]
|
||||
statuses = {row["status"] for row in rows}
|
||||
assert statuses == {5}, f"清空方案后旧在途单应撤回且无新单:{statuses}"
|
||||
assert (
|
||||
client.get(f"{_APPROVALS}/{first['id']}", headers=finance["headers"]).json()["data"][
|
||||
"status"
|
||||
]
|
||||
== 5
|
||||
)
|
||||
|
||||
def test_删除业务在途单禁删_终态单随删(
|
||||
self, client: TestClient, admin: dict, finance, switched_on
|
||||
) -> None:
|
||||
"""ref_id 固化无外键:在途单禁删(防指向已删业务的活单继续被审批),
|
||||
终态单随业务删除一并清理(已通过/已驳回的历史事实由留痕日志承载)。"""
|
||||
p = _person(client, admin["headers"])
|
||||
data = _create_with_plan(client, admin["headers"], p["id"])
|
||||
|
||||
# 在途:禁删
|
||||
resp = client.delete(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"])
|
||||
assert resp.status_code == 400
|
||||
assert "在途审批单" in resp.json()["message"]
|
||||
|
||||
# 驳回(终态)后:可删,且审批单随之清理
|
||||
order = _latest_order(client, finance["headers"], data["business_no"])
|
||||
client.post(
|
||||
f"{_APPROVALS}/{order['id']}/action",
|
||||
json={"action": 2, "opinion": "驳回后可删"},
|
||||
headers=finance["headers"],
|
||||
)
|
||||
resp = client.delete(f"{_BUSINESSES}/{data['id']}", headers=admin["headers"])
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
rows = client.get(
|
||||
_APPROVALS,
|
||||
params={"keyword": data["business_no"]},
|
||||
headers=finance["headers"],
|
||||
).json()["data"]["list"]
|
||||
assert rows == [], "终态审批单应随业务删除一并清理"
|
||||
|
||||
|
||||
class TestModuleAccess:
|
||||
"""模块准入:普通用户与营销后勤不可见(权限清单待澄清 #6)。"""
|
||||
|
||||
@@ -142,6 +142,43 @@ class TestGuards:
|
||||
assert resp.status_code == 400
|
||||
assert "分摊参与" in resp.json()["message"]
|
||||
|
||||
def test_离职历史参与人_编辑其余字段放行(self, client: TestClient, admin: dict) -> None:
|
||||
"""历史参与人离职后:编辑业务(保留该行)不应被 400 卡死——
|
||||
personnel_name 冗余固化的设计本就允许离职后回显;仅新增参与人
|
||||
才做在职校验。"""
|
||||
p = _person(client, admin["headers"])
|
||||
data = _create_with_allocations(
|
||||
client, admin["headers"], [{"personnel_id": p["id"], "ratio": "50.00"}]
|
||||
)
|
||||
resp = client.put(
|
||||
f"{_PERSONNEL}/{p['id']}/status", json={"status": 0}, headers=admin["headers"]
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
detail = _detail(client, admin["headers"], data["id"])
|
||||
payload = business_payload(
|
||||
detail["customer_id"],
|
||||
secondary_allocations=[{"personnel_id": p["id"], "ratio": "50.00"}],
|
||||
)
|
||||
resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"])
|
||||
assert resp.status_code == 200, resp.text
|
||||
after = _detail(client, admin["headers"], data["id"])
|
||||
assert after["secondary_allocations"][0]["personnel_name"] == p["personnel_name"]
|
||||
|
||||
# 新增一名离职人员仍要拦(新增才做在职校验)
|
||||
p2 = _person(client, admin["headers"])
|
||||
client.put(f"{_PERSONNEL}/{p2['id']}/status", json={"status": 0}, headers=admin["headers"])
|
||||
payload = business_payload(
|
||||
detail["customer_id"],
|
||||
secondary_allocations=[
|
||||
{"personnel_id": p["id"], "ratio": "30.00"},
|
||||
{"personnel_id": p2["id"], "ratio": "20.00"},
|
||||
],
|
||||
)
|
||||
resp = client.put(f"{_BUSINESSES}/{data['id']}", json=payload, headers=admin["headers"])
|
||||
assert resp.status_code == 400
|
||||
assert "分摊参与" in resp.json()["message"]
|
||||
|
||||
def test_终态业务禁改方案(self, client: TestClient, admin: dict) -> None:
|
||||
p = _person(client, admin["headers"])
|
||||
data = _create_with_allocations(
|
||||
|
||||
Reference in new issue
Block a user