398 lines
16 KiB
Python
398 lines
16 KiB
Python
"""ORD-001 ~ ORD-022: 订单录入、提交、审批测试。
|
||
|
||
覆盖功能点:
|
||
- 订单创建(草稿、新客户自动入库、利润计算)
|
||
- 订单提交审核(成功、重复提交)
|
||
- 订单审批(通过、退回)
|
||
- 订单下发(生成文本、确认下发)
|
||
- 订单详情与字段权限
|
||
- 订单编辑
|
||
- 订单状态流转完整性
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderCreation:
|
||
"""ORD-001 ~ ORD-006: 订单创建测试。"""
|
||
|
||
def test_create_draft_order(self, client, salesman_headers, make_product):
|
||
"""ORD-001: 创建草稿订单成功。"""
|
||
product = make_product()
|
||
resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "新客户张三",
|
||
"customer_mobile": "13800001234",
|
||
"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()
|
||
assert data["code"] == 0
|
||
# API 创建订单后自动提交审批,初始状态为 pending_approve
|
||
assert data["data"]["order_status"] == "pending_approve"
|
||
assert data["data"]["order_no"] is not None
|
||
|
||
def test_new_customer_auto_created(self, client, salesman_headers, make_product, db_session):
|
||
"""ORD-002: 新客户自动入库。"""
|
||
from backend.app.models.business import Customer
|
||
product = make_product()
|
||
client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "自动入库客户",
|
||
"customer_mobile": "13800009999",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": product.specification,
|
||
"unit": product.unit,
|
||
"quantity": 5,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
customer = db_session.query(Customer).filter(
|
||
Customer.mobile == "13800009999"
|
||
).first()
|
||
assert customer is not None
|
||
assert customer.customer_name == "自动入库客户"
|
||
|
||
def test_profit_calculation(self, client, salesman_headers, make_product):
|
||
"""ORD-003: 利润计算验证。"""
|
||
product = make_product(cost_price=60, sale_price=100)
|
||
resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "利润测试客户",
|
||
"customer_mobile": "13800007777",
|
||
"rebate_total": 5,
|
||
"freight_total": 10,
|
||
"tax_total": 3,
|
||
"other_fee_total": 2,
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": "100x100cm",
|
||
"unit": "张",
|
||
"quantity": 1,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
"rebate_amount": 5,
|
||
"freight_amount": 10,
|
||
"tax_amount": 3,
|
||
"other_fee_amount": 2,
|
||
}],
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()["data"]
|
||
# 利润 = 100 - 60 - 5 - 10 - 3 - 2 = 20
|
||
assert float(data["profit_total"]) == 20
|
||
# 利润率 = 20 / 100 * 100 = 20%
|
||
assert float(data["profit_rate"]) == 20
|
||
|
||
def test_create_order_no_items(self, client, salesman_headers):
|
||
"""ORD-005: 明细为空应返回 40001。"""
|
||
resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "空明细客户",
|
||
"customer_mobile": "13800006666",
|
||
"items": [],
|
||
})
|
||
assert resp.status_code == 400
|
||
data = resp.json()
|
||
assert data["code"] == 40001
|
||
|
||
def test_create_order_zero_quantity(self, client, salesman_headers, make_product):
|
||
"""ORD-006: 数量为 0 应返回验证错误。
|
||
|
||
注意: Pydantic Field(gt=0) 校验返回 422,自定义校验返回 400
|
||
"""
|
||
product = make_product()
|
||
resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "零数量客户",
|
||
"customer_mobile": "13800005555",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": product.specification,
|
||
"unit": product.unit,
|
||
"quantity": 0,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
assert resp.status_code in (400, 422)
|
||
|
||
def test_disabled_product_cannot_order(self, client, salesman_headers, make_product):
|
||
"""ORD-021: 禁用产品不能创建订单。"""
|
||
product = make_product(status=0)
|
||
resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "禁用产品客户",
|
||
"customer_mobile": "13800005555",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": product.specification,
|
||
"unit": product.unit,
|
||
"quantity": 1,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
# 应被拒绝或有警告
|
||
assert resp.status_code in (400, 200)
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderSubmit:
|
||
"""ORD-007 ~ ORD-008: 订单提交审核测试。"""
|
||
|
||
def _create_order(self, client, salesman_headers, make_product):
|
||
"""辅助方法:创建订单并返回 order_id。"""
|
||
product = make_product()
|
||
resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "提交测试客户",
|
||
"customer_mobile": "13800004444",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": "100x100cm",
|
||
"unit": "张",
|
||
"quantity": 10,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
assert resp.status_code == 200, f"创建订单失败: {resp.json()}"
|
||
return resp.json()["data"]["order_id"]
|
||
|
||
def test_submit_order_success(self, client, salesman_headers, make_product):
|
||
"""ORD-007: 创建订单后自动提交审核,状态为 pending_approve。"""
|
||
order_id = self._create_order(client, salesman_headers, make_product)
|
||
# API 创建订单时自动提交审批,无需再次 submit
|
||
resp = client.get(f"/api/orders/{order_id}", headers=salesman_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["data"]["order_status"] == "pending_approve"
|
||
|
||
def test_double_submit(self, client, salesman_headers, manager_headers, make_product):
|
||
"""ORD-008: 对已审批通过的订单重复提交应返回错误。"""
|
||
order_id = self._create_order(client, salesman_headers, make_product)
|
||
# 订单创建时已是 pending_approve,用经理权限审批通过
|
||
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
||
"approve_result": "pass",
|
||
})
|
||
# 审批通过后尝试再次提交
|
||
resp = client.post(f"/api/orders/{order_id}/submit", headers=salesman_headers)
|
||
assert resp.status_code in (400, 409)
|
||
|
||
def test_list_orders(self, client, salesman_headers):
|
||
"""ORD-014: 订单列表查询。"""
|
||
resp = client.get("/api/orders", headers=salesman_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "total" in data["data"]
|
||
assert "list" in data["data"]
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderApproval:
|
||
"""ORD-009 ~ ORD-010: 订单审批测试。"""
|
||
|
||
def _create_and_submit(self, client, salesman_headers, make_product):
|
||
"""辅助方法:创建订单并提交审核。"""
|
||
product = make_product()
|
||
create_resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "审批测试客户",
|
||
"customer_mobile": "13800002222",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": "100x100cm",
|
||
"unit": "张",
|
||
"quantity": 10,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
order_id = create_resp.json()["data"]["order_id"]
|
||
client.post(f"/api/orders/{order_id}/submit", headers=salesman_headers)
|
||
return order_id
|
||
|
||
def test_approve_order_success(self, client, salesman_headers, manager_headers, make_product):
|
||
"""ORD-009: 管理层审批通过。"""
|
||
order_id = self._create_and_submit(client, salesman_headers, make_product)
|
||
resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
||
"approve_result": "pass",
|
||
"approve_opinion": "同意",
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["data"]["order_status"] == "approved"
|
||
|
||
def test_reject_order_success(self, client, salesman_headers, manager_headers, make_product):
|
||
"""ORD-010: 管理层审批退回。"""
|
||
order_id = self._create_and_submit(client, salesman_headers, make_product)
|
||
resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
||
"approve_result": "reject",
|
||
"approve_opinion": "价格需要重新确认",
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["data"]["order_status"] == "rejected"
|
||
|
||
def test_approve_non_pending_order(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)
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderSupplierText:
|
||
"""ORD-011 ~ ORD-013: 订单下发测试。"""
|
||
|
||
def test_generate_supplier_text(self, client, manager_headers, make_order):
|
||
"""ORD-011: 生成下发文本。"""
|
||
order = make_order(order_status="approved")
|
||
resp = client.post(f"/api/orders/{order.id}/supplier-text", headers=manager_headers, json={
|
||
"supplier_id": order.factory_id,
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "text_content" in data["data"]
|
||
|
||
def test_confirm_supplier_text(self, client, manager_headers, make_order):
|
||
"""ORD-012: 确认下发工厂。"""
|
||
order = make_order(order_status="approved")
|
||
resp = client.post(f"/api/orders/{order.id}/supplier-text/confirm", headers=manager_headers, json={
|
||
"supplier_id": order.factory_id,
|
||
"text_content": "测试下发文本内容",
|
||
})
|
||
assert resp.status_code == 200
|
||
|
||
def test_confirm_text_not_approved(self, client, manager_headers, make_order):
|
||
"""ORD-013: 非 approved 状态不能确认下发。"""
|
||
order = make_order(order_status="draft")
|
||
resp = client.post(f"/api/orders/{order.id}/supplier-text/confirm", headers=manager_headers, json={
|
||
"supplier_id": order.factory_id,
|
||
"text_content": "不应该成功",
|
||
})
|
||
assert resp.status_code in (400, 409)
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderDetail:
|
||
"""ORD-015 ~ ORD-016: 订单详情与字段权限测试。"""
|
||
|
||
def test_salesman_order_detail_no_finance(self, client, salesman_headers, make_order):
|
||
"""ORD-015: 业务员订单详情不应包含成本、利润字段。"""
|
||
order = make_order(salesman_id=2)
|
||
resp = client.get(f"/api/orders/{order.id}", headers=salesman_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()["data"]
|
||
# 业务员不应看到成本、利润
|
||
assert "cost_price_total" not in data or data.get("cost_price_total") is None or float(data.get("cost_price_total", 0)) == 0
|
||
|
||
def test_manager_order_detail_with_finance(self, client, manager_headers, make_order):
|
||
"""ORD-016: 管理层订单详情应包含完整财务字段。"""
|
||
order = make_order()
|
||
resp = client.get(f"/api/orders/{order.id}", headers=manager_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()["data"]
|
||
# 管理层应该能看到成本、利润
|
||
assert "profit_total" in data
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderEdit:
|
||
"""ORD-017 ~ ORD-018: 订单编辑测试。"""
|
||
|
||
def test_edit_draft_order(self, client, salesman_headers, make_product):
|
||
"""ORD-017: 编辑草稿订单成功。"""
|
||
product = make_product()
|
||
create_resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "编辑测试客户",
|
||
"customer_mobile": "13800001111",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": "100x100cm",
|
||
"unit": "张",
|
||
"quantity": 5,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
order_id = create_resp.json()["data"]["order_id"]
|
||
|
||
resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={
|
||
"customer_name": "编辑后客户名",
|
||
"customer_mobile": "13800001111",
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": "100x100cm",
|
||
"unit": "张",
|
||
"quantity": 20,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
assert resp.status_code == 200
|
||
|
||
|
||
@pytest.mark.order
|
||
@pytest.mark.p0
|
||
class TestOrderStatusFlow:
|
||
"""ORD-022: 订单状态流转完整性测试。"""
|
||
|
||
def test_full_status_flow(self, client, salesman_headers, manager_headers, driver_headers,
|
||
make_product, make_supplier):
|
||
"""完整状态流转: draft → pending_approve → approved → pending_factory"""
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
|
||
# 1. 创建订单(API 自动提交审批,初始状态为 pending_approve)
|
||
create_resp = client.post("/api/orders", headers=salesman_headers, json={
|
||
"customer_name": "状态流转客户",
|
||
"customer_mobile": "13800008888",
|
||
"factory_id": supplier.id,
|
||
"items": [{
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"specification": "100x100cm",
|
||
"unit": "张",
|
||
"quantity": 10,
|
||
"sale_price": 100,
|
||
"cost_price": 60,
|
||
}],
|
||
})
|
||
order_id = create_resp.json()["data"]["order_id"]
|
||
assert create_resp.json()["data"]["order_status"] == "pending_approve"
|
||
|
||
# 2. 审批通过
|
||
approve_resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
|
||
"approve_result": "pass",
|
||
})
|
||
assert approve_resp.json()["data"]["order_status"] == "approved"
|
||
|
||
# 4. 确认下发
|
||
confirm_resp = client.post(f"/api/orders/{order_id}/supplier-text/confirm", headers=manager_headers, json={
|
||
"supplier_id": supplier.id,
|
||
"text_content": "下发文本",
|
||
})
|
||
assert confirm_resp.status_code == 200
|