64dc85bd14
- inventory业务层下沉(薄路由+service orchestration模式): - customer/supplier/warehouse -> master_data_service - material_routes(价格历史/趋势/供应商关联)-> material_service - product_routes(CRUD/BOM/from-task跨模块桥接)-> product_service - dashboard_routes(首页统计/低库存预警)-> dashboard_service - inventory侧新增service回归覆盖(dashboard 2 / master_data 10 / material 10 / product 14),含跨模块桥接测试种子 - Pydantic v2弃用清零:全仓14处 class Config 全部迁移到 model_config = ConfigDict(from_attributes=True)(含 shared auth) - datetime.utcnow() 弃用清零:auth_service 3处统一改 datetime.now(timezone.utc) - 同步文档:STATUS / ROADMAP / TECH_DEBT(D12清偿)/ AGENTS 代码地图 测试基线:126 passed, 4 skipped(无deprecation warning) Co-Authored-By: Claude Code <noreply@anthropic.com>
177 lines
5.4 KiB
Python
177 lines
5.4 KiB
Python
import pytest
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_list_products_returns_material_cost_for_finished_goods(client):
|
|
resp = await client.get("/api/products")
|
|
assert resp.status_code == 200
|
|
rows = resp.json()
|
|
finished = next(row for row in rows if row["sku"] == "MOLD-STD")
|
|
assert float(finished["material_cost"]) == 20.0
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_create_product_finished_resets_stock_bounds(client):
|
|
resp = await client.post(
|
|
"/api/products",
|
|
json={
|
|
"sku": "FG-NEW",
|
|
"name": "新成品",
|
|
"item_type": "finished",
|
|
"unit": "件",
|
|
"min_stock": 10,
|
|
"max_stock": 99,
|
|
"cost_price": 12,
|
|
"sale_price": 20,
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert body["sku"] == "FG-NEW"
|
|
assert body["min_stock"] == 0
|
|
assert body["max_stock"] == 0
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_update_product_rejects_duplicate_sku(client):
|
|
created = await client.post(
|
|
"/api/products",
|
|
json={
|
|
"sku": "FG-2",
|
|
"name": "成品2",
|
|
"item_type": "finished",
|
|
"unit": "件",
|
|
"cost_price": 0,
|
|
"sale_price": 1,
|
|
},
|
|
)
|
|
assert created.status_code == 201
|
|
product_id = created.json()["id"]
|
|
|
|
resp = await client.put(
|
|
f"/api/products/{product_id}",
|
|
json={
|
|
"sku": "MOLD-STD",
|
|
"name": "改名",
|
|
"item_type": "finished",
|
|
"unit": "件",
|
|
"cost_price": 0,
|
|
"sale_price": 1,
|
|
},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert resp.json()["detail"] == "SKU已存在"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_delete_product_soft_deletes_record(client):
|
|
created = await client.post(
|
|
"/api/products",
|
|
json={
|
|
"sku": "MAT-DEL",
|
|
"name": "待删物料",
|
|
"item_type": "material",
|
|
"unit": "kg",
|
|
"cost_price": 2,
|
|
"sale_price": 0,
|
|
},
|
|
)
|
|
assert created.status_code == 201
|
|
product_id = created.json()["id"]
|
|
|
|
deleted = await client.delete(f"/api/products/{product_id}")
|
|
assert deleted.status_code == 200
|
|
assert deleted.json()["message"] == "产品已删除"
|
|
|
|
listed = await client.get("/api/products")
|
|
assert listed.status_code == 200
|
|
assert all(row["id"] != product_id for row in listed.json())
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_get_product_bom_returns_line_costs(client):
|
|
resp = await client.get("/api/products/2/materials")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["product_id"] == 2
|
|
assert float(body["total_material_cost"]) == 20.0
|
|
assert len(body["items"]) == 1
|
|
assert body["items"][0]["material_sku"] == "MAT-001"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_replace_product_bom_rewrites_items(client):
|
|
created = await client.post(
|
|
"/api/products",
|
|
json={
|
|
"sku": "MAT-002",
|
|
"name": "铝材",
|
|
"item_type": "material",
|
|
"unit": "kg",
|
|
"cost_price": 5,
|
|
"sale_price": 0,
|
|
},
|
|
)
|
|
assert created.status_code == 201
|
|
material_id = created.json()["id"]
|
|
|
|
replaced = await client.put(
|
|
"/api/products/2/materials",
|
|
json={
|
|
"items": [
|
|
{"material_id": 1, "quantity": 1, "loss_rate": 0},
|
|
{"material_id": material_id, "quantity": 2, "loss_rate": 0.1},
|
|
]
|
|
},
|
|
)
|
|
assert replaced.status_code == 200
|
|
body = replaced.json()
|
|
assert len(body["items"]) == 2
|
|
assert float(body["total_material_cost"]) == 20.0
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize(
|
|
"url,payload,expected",
|
|
[
|
|
("/api/products", {"sku": "BAD", "name": "bad", "item_type": "unknown", "unit": "件", "cost_price": 0, "sale_price": 0}, 400),
|
|
("/api/products/2/materials", {"items": [{"material_id": 1, "quantity": 1}, {"material_id": 1, "quantity": 2}]}, 400),
|
|
("/api/products/2/materials", {"items": [{"material_id": 99999, "quantity": 1}]}, 400),
|
|
("/api/products/2/materials", {"items": [{"material_id": 2, "quantity": 1}]}, 400),
|
|
("/api/products/2/materials", {"items": [{"material_id": 1, "quantity": 0}]}, 400),
|
|
("/api/products/1/materials", {"items": []}, 400),
|
|
],
|
|
)
|
|
async def test_product_service_validation_paths(url, payload, expected, client):
|
|
method = client.post if url == "/api/products" else client.put
|
|
resp = await method(url, json=payload)
|
|
assert resp.status_code == expected
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_create_product_from_task_creates_and_binds_product(client):
|
|
resp = await client.post("/api/products/from-task/task-demo-1")
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert body["sku"] == "MI1"
|
|
assert body["name"] == "demo-mold"
|
|
assert body["category"] == "模具成品"
|
|
assert body["item_type"] == "finished"
|
|
assert body["min_stock"] == 0
|
|
assert body["max_stock"] == 0
|
|
assert "由模具分析创建" in body["description"]
|
|
|
|
again = await client.post("/api/products/from-task/task-demo-1")
|
|
assert again.status_code == 201
|
|
assert again.json()["id"] == body["id"]
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize(
|
|
"task_id,expected",
|
|
[("missing-task", 404)],
|
|
)
|
|
async def test_create_product_from_task_validation(task_id, expected, client):
|
|
resp = await client.post(f"/api/products/from-task/{task_id}")
|
|
assert resp.status_code == expected
|