537 lines
21 KiB
Python
537 lines
21 KiB
Python
"""订单流转节点逐点验证测试。
|
|
|
|
逐一测试订单生命周期中的每个状态转换节点,
|
|
确保每个节点都能正常工作。
|
|
|
|
流程图:
|
|
创建 -> pending_approve -> approved -> pending_factory -> pending_driver
|
|
-> accepted -> picked_up -> delivered -> completed -> settled
|
|
|
|
取消流:
|
|
pending_approve -> canceled (直接取消)
|
|
approved -> cancel_pending -> canceled (审批取消)
|
|
pending_factory -> cancel_fulfillment_pending -> canceled (履约取消)
|
|
pending_factory -> cancel_fulfillment_pending -> pending_factory (拒绝取消恢复)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode1_Create:
|
|
"""节点1: 创建订单 -> pending_approve"""
|
|
|
|
def test_create_order_goes_to_pending_approve(self, client, salesman_headers, make_product):
|
|
"""创建订单后状态应为 pending_approve。"""
|
|
product = make_product()
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "节点测试客户",
|
|
"customer_mobile": "13800001001",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
assert resp.status_code == 200
|
|
data = resp.json()["data"]
|
|
assert data["order_status"] == "pending_approve"
|
|
assert data["order_no"] is not None
|
|
return data["order_id"]
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode2_Approve:
|
|
"""节点2: pending_approve -> approved (审批通过)"""
|
|
|
|
def test_approve_passes_order(self, client, salesman_headers, manager_headers, make_product):
|
|
"""审批通过后状态应为 approved。"""
|
|
product = make_product()
|
|
# 创建
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "审批测试客户",
|
|
"customer_mobile": "13800001002",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
order_id = resp.json()["data"]["order_id"]
|
|
|
|
# 审批通过
|
|
resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "approved"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode2b_Reject:
|
|
"""节点2b: pending_approve -> rejected (审批拒绝)"""
|
|
|
|
def test_reject_order(self, client, salesman_headers, manager_headers, make_product):
|
|
"""审批拒绝后状态应为 rejected。"""
|
|
product = make_product()
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "拒绝测试客户",
|
|
"customer_mobile": "13800001003",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
order_id = resp.json()["data"]["order_id"]
|
|
|
|
resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
|
"approve_result": "reject",
|
|
"approve_opinion": "价格不合理",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "rejected"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode2c_RejectResubmit:
|
|
"""节点2c: rejected -> pending_approve (退回后重新提交)"""
|
|
|
|
def test_reject_then_resubmit(self, client, salesman_headers, manager_headers, make_product):
|
|
"""退回后重新提交应恢复为 pending_approve。"""
|
|
product = make_product()
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "重提测试客户",
|
|
"customer_mobile": "13800001004",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
order_id = resp.json()["data"]["order_id"]
|
|
|
|
# 拒绝
|
|
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
|
"approve_result": "reject",
|
|
"approve_opinion": "需修改",
|
|
})
|
|
|
|
# 重新提交
|
|
resp = client.post(f"/api/orders/{order_id}/submit", headers=salesman_headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "pending_approve"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode3_SupplierText:
|
|
"""节点3: approved -> pending_factory (确认供应商文本)"""
|
|
|
|
def test_confirm_supplier_text_moves_to_pending_factory(self, client, salesman_headers, manager_headers, make_product, make_supplier):
|
|
"""确认供应商文本后状态应为 pending_factory。"""
|
|
product = make_product()
|
|
supplier = make_supplier()
|
|
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "下发测试客户",
|
|
"customer_mobile": "13800001005",
|
|
"factory_id": supplier.id,
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
order_id = resp.json()["data"]["order_id"]
|
|
|
|
# 审批通过
|
|
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
|
|
# 确认供应商文本
|
|
resp = client.post(f"/api/orders/{order_id}/supplier-text/confirm", headers=manager_headers, json={
|
|
"supplier_id": supplier.id,
|
|
"text_content": "请安排生产10张产品",
|
|
})
|
|
assert resp.status_code == 200
|
|
# 检查订单状态
|
|
detail = client.get(f"/api/orders/{order_id}", headers=manager_headers)
|
|
assert detail.json()["data"]["order_status"] == "pending_factory"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode4_ManualStatusChanges:
|
|
"""节点4-8: pending_factory -> pending_driver -> accepted -> picked_up -> delivered -> completed -> settled"""
|
|
|
|
def test_full_fulfillment_pipeline(self, client, manager_headers, make_order):
|
|
"""完整履约流程: pending_factory -> pending_driver -> accepted -> picked_up -> delivered -> completed -> settled。"""
|
|
order = make_order(order_status="pending_factory")
|
|
order_id = order.id
|
|
|
|
transitions = [
|
|
("pending_driver", "分配司机"),
|
|
("accepted", "司机接单"),
|
|
("picked_up", "司机提货"),
|
|
("delivered", "送达客户"),
|
|
("completed", "订单完成"),
|
|
("settled", "订单结算"),
|
|
]
|
|
|
|
for target_status, remark in transitions:
|
|
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
|
|
"target_status": target_status,
|
|
"remark": remark,
|
|
})
|
|
assert resp.status_code == 200, f"转换到 {target_status} 失败: {resp.text}"
|
|
assert resp.json()["data"]["order_status"] == target_status, \
|
|
f"期望 {target_status}, 实际 {resp.json()['data']['order_status']}"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderFlowNode5_ProductionTrack:
|
|
"""节点5: pending_factory -> production -> shipped -> delivered"""
|
|
|
|
def test_production_track(self, client, manager_headers, make_order):
|
|
"""生产流程: pending_factory -> production -> shipped -> delivered。"""
|
|
order = make_order(order_status="pending_factory")
|
|
order_id = order.id
|
|
|
|
# 转到生产
|
|
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
|
|
"target_status": "production",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "production"
|
|
|
|
# 发货
|
|
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
|
|
"target_status": "shipped",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "shipped"
|
|
|
|
# 送达
|
|
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
|
|
"target_status": "delivered",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "delivered"
|
|
|
|
|
|
@pytest.mark.cancel
|
|
@pytest.mark.p0
|
|
class TestCancelNode1_DirectCancel:
|
|
"""取消节点1: pending_approve -> canceled (直接取消)"""
|
|
|
|
def test_direct_cancel_from_pending_approve(self, client, salesman_headers, make_order):
|
|
"""待审批订单可直接取消。"""
|
|
order = make_order(order_status="pending_approve", salesman_id=2)
|
|
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
|
|
"cancel_reason": "客户取消",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "canceled"
|
|
|
|
|
|
@pytest.mark.cancel
|
|
@pytest.mark.p0
|
|
class TestCancelNode2_ApprovedCancel:
|
|
"""取消节点2: approved -> cancel_pending -> canceled (审批取消)"""
|
|
|
|
def test_approved_cancel_flow(self, client, salesman_headers, manager_headers, make_order):
|
|
"""已审批订单取消需要经理批准。"""
|
|
order = make_order(order_status="approved", salesman_id=2)
|
|
|
|
# 申请取消
|
|
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
|
|
"cancel_reason": "客户变更需求",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "cancel_pending"
|
|
|
|
# 经理批准取消
|
|
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "canceled"
|
|
|
|
def test_approved_cancel_refuse_restores(self, client, salesman_headers, manager_headers, make_order):
|
|
"""拒绝取消应恢复到原状态。"""
|
|
order = make_order(order_status="approved", salesman_id=2)
|
|
|
|
# 申请取消
|
|
client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
|
|
"cancel_reason": "客户变更需求",
|
|
})
|
|
|
|
# 经理拒绝取消
|
|
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
|
|
"approve_result": "refuse",
|
|
"approve_opinion": "不同意取消",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "approved"
|
|
|
|
|
|
@pytest.mark.cancel
|
|
@pytest.mark.p0
|
|
class TestCancelNode3_FulfillmentCancel:
|
|
"""取消节点3: pending_factory -> cancel_fulfillment_pending -> canceled (履约取消)"""
|
|
|
|
def test_fulfillment_cancel_flow(self, client, salesman_headers, manager_headers, make_order):
|
|
"""履约阶段取消需要经理批准。"""
|
|
order = make_order(order_status="pending_factory", salesman_id=2)
|
|
|
|
# 申请取消
|
|
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
|
|
"cancel_reason": "客户取消订单",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "cancel_fulfillment_pending"
|
|
|
|
# 经理批准
|
|
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "canceled"
|
|
|
|
def test_fulfillment_cancel_refuse_restores(self, client, salesman_headers, manager_headers, make_order):
|
|
"""拒绝履约取消应恢复到原状态。"""
|
|
order = make_order(order_status="pending_factory", salesman_id=2)
|
|
|
|
# 申请取消
|
|
client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
|
|
"cancel_reason": "客户取消订单",
|
|
})
|
|
|
|
# 经理拒绝
|
|
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
|
|
"approve_result": "refuse",
|
|
"approve_opinion": "已安排生产",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "pending_factory"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestInvalidTransitions:
|
|
"""非法状态转换应被拒绝。"""
|
|
|
|
def test_cannot_approve_draft(self, client, manager_headers, make_order):
|
|
"""草稿订单不能审批。"""
|
|
order = make_order(order_status="draft")
|
|
resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code in (400, 409)
|
|
|
|
def test_cannot_approve_canceled(self, client, manager_headers, make_order):
|
|
"""已取消订单不能审批。"""
|
|
order = make_order(order_status="canceled")
|
|
resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code in (400, 409)
|
|
|
|
def test_cannot_submit_approved(self, client, salesman_headers, make_order):
|
|
"""已审批订单不能再次提交。"""
|
|
order = make_order(order_status="approved", salesman_id=2)
|
|
resp = client.post(f"/api/orders/{order.id}/submit", headers=salesman_headers)
|
|
assert resp.status_code in (400, 409)
|
|
|
|
def test_cannot_skip_to_delivered(self, client, manager_headers, make_order):
|
|
"""pending_factory 不能直接跳到 delivered。"""
|
|
order = make_order(order_status="pending_factory")
|
|
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
|
|
"target_status": "delivered",
|
|
})
|
|
assert resp.status_code in (400, 409)
|
|
|
|
def test_cannot_cancel_settled(self, client, salesman_headers, make_order):
|
|
"""已结算订单不能取消。"""
|
|
order = make_order(order_status="settled", salesman_id=2)
|
|
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
|
|
"cancel_reason": "测试",
|
|
})
|
|
assert resp.status_code in (400, 409)
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderEditNodes:
|
|
"""编辑节点测试。"""
|
|
|
|
def test_edit_pending_approve_order(self, client, salesman_headers, make_product):
|
|
"""pending_approve 状态可编辑。"""
|
|
product = make_product()
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "编辑测试客户",
|
|
"customer_mobile": "13800001010",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
order_id = resp.json()["data"]["order_id"]
|
|
|
|
# 编辑
|
|
resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={
|
|
"customer_name": "编辑后的客户",
|
|
"customer_mobile": "13800001010",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 20,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "pending_approve"
|
|
|
|
def test_edit_rejected_order(self, client, salesman_headers, manager_headers, make_product):
|
|
"""rejected 状态可编辑。"""
|
|
product = make_product()
|
|
resp = client.post("/api/orders", headers=salesman_headers, json={
|
|
"customer_name": "退回编辑客户",
|
|
"customer_mobile": "13800001011",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 10,
|
|
"sale_price": 100,
|
|
"cost_price": 60,
|
|
}],
|
|
})
|
|
order_id = resp.json()["data"]["order_id"]
|
|
|
|
# 拒绝
|
|
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
|
"approve_result": "reject",
|
|
"approve_opinion": "需修改",
|
|
})
|
|
|
|
# 编辑
|
|
resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={
|
|
"customer_name": "退回编辑后的客户",
|
|
"customer_mobile": "13800001011",
|
|
"items": [{
|
|
"product_id": product.id,
|
|
"product_name": product.product_name,
|
|
"specification": product.specification,
|
|
"unit": product.unit,
|
|
"quantity": 15,
|
|
"sale_price": 120,
|
|
"cost_price": 70,
|
|
}],
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"]["order_status"] == "rejected"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderSupplierTextNode:
|
|
"""供应商文本节点测试。"""
|
|
|
|
def test_generate_supplier_text(self, client, manager_headers, make_order, make_supplier):
|
|
"""生成供应商文本(不改变状态)。"""
|
|
supplier = make_supplier()
|
|
order = make_order(order_status="approved")
|
|
order.factory_id = supplier.id
|
|
|
|
resp = client.post(f"/api/orders/{order.id}/supplier-text", headers=manager_headers, json={
|
|
"supplier_id": supplier.id,
|
|
})
|
|
assert resp.status_code == 200
|
|
# 生成文本不改变状态
|
|
detail = client.get(f"/api/orders/{order.id}", headers=manager_headers)
|
|
assert detail.json()["data"]["order_status"] == "approved"
|
|
|
|
|
|
@pytest.mark.order
|
|
@pytest.mark.p0
|
|
class TestOrderPermissionNodes:
|
|
"""权限节点测试。"""
|
|
|
|
def test_salesman_cannot_approve(self, client, salesman_headers, make_order):
|
|
"""业务员不能审批。"""
|
|
order = make_order(order_status="pending_approve", salesman_id=2)
|
|
resp = client.post(f"/api/orders/{order.id}/approve", headers=salesman_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code == 403
|
|
|
|
def test_salesman_cannot_change_status(self, client, salesman_headers, make_order):
|
|
"""业务员不能手动变更状态。"""
|
|
order = make_order(order_status="pending_factory", salesman_id=2)
|
|
resp = client.post(f"/api/orders/{order.id}/status", headers=salesman_headers, json={
|
|
"target_status": "pending_driver",
|
|
})
|
|
assert resp.status_code == 403
|
|
|
|
def test_salesman_cannot_cancel_approve(self, client, salesman_headers, make_order):
|
|
"""业务员不能审批取消。"""
|
|
order = make_order(order_status="cancel_pending", salesman_id=2)
|
|
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=salesman_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code == 403
|
|
|
|
def test_manager_can_approve(self, client, manager_headers, make_order):
|
|
"""经理可以审批。"""
|
|
order = make_order(order_status="pending_approve")
|
|
resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={
|
|
"approve_result": "pass",
|
|
})
|
|
assert resp.status_code == 200
|
|
|
|
def test_manager_can_change_status(self, client, manager_headers, make_order):
|
|
"""经理可以变更状态。"""
|
|
order = make_order(order_status="pending_factory")
|
|
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
|
|
"target_status": "pending_driver",
|
|
})
|
|
assert resp.status_code == 200
|