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