feat(backend): 出入库搜索重构 + 商品编码搜索 + KPI/确认售价/详细搜索
- 搜索 keyword 命中改为 单号/酒名(名+拼音)/商品编码,去掉往来单位匹配 - applyStockOrderDetailFilters 合并明细过滤为单条子查询(同一明细语义)+ 新增 product_code - 新增出入库 KPI summary、出库确认售价(confirm-sale) 及路由 - 补充/更新对应单元测试 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -156,14 +156,19 @@ func TestStockOutHandler_List_FilterByProductCode(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
// 精确命中 1 单
|
||||
// 命中 1 单
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=ZXZ-FIND", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 不存在的编码 → 0;前缀不算精确(不命中)
|
||||
// 商品编码改为子串 LIKE(详细搜索友好):子串 ZXZ → 命中 ZXZ-FIND
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=ZXZ", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 不存在的编码 → 0
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=NOPE", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"].(float64))
|
||||
}
|
||||
|
||||
@@ -199,9 +204,13 @@ func TestStockOutHandler_List_FilterByKeyword(t *testing.T) {
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
// 按往来单位名搜索
|
||||
// 往来单位名不再进入 keyword 范围(供应商/客户改由工具栏下拉筛选)→ 搜不到
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=海底捞", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 按酒名搜索(两单都含 Beer)
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=Beer", token, nil)
|
||||
assert.Equal(t, float64(2), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 按单号搜索
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword="+order1No, token, nil)
|
||||
@@ -460,3 +469,80 @@ func TestStockOutHandler_InventoryLog_OnApprove(t *testing.T) {
|
||||
assert.Equal(t, float64(100), logs[0].QtyBefore)
|
||||
assert.Equal(t, float64(80), logs[0].QtyAfter)
|
||||
}
|
||||
|
||||
func TestStockOutHandler_Summary(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SOSUM")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
wh := testutil.CreateTestWarehouse(db, shop.ID, "W")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Wine")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &product.ID, Quantity: 100})
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 1.0, "unit_price": 5.0, "sale_price": 100.0}},
|
||||
})
|
||||
}
|
||||
var first model.StockOutOrder
|
||||
db.Where("shop_id = ?", shop.ID).Order("id ASC").First(&first)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", first.ID), token, nil)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/summary", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
s := parseResponse(w)
|
||||
assert.Equal(t, float64(2), s["month_count"])
|
||||
assert.Equal(t, float64(200), s["month_amount"]) // 2 单 × (1×100 售价)
|
||||
assert.Equal(t, float64(1), s["pending_count"])
|
||||
}
|
||||
|
||||
func TestStockOutHandler_ConfirmSale(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SOCS")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
wh := testutil.CreateTestWarehouse(db, shop.ID, "W")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Whiskey")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &product.ID, Quantity: 100})
|
||||
|
||||
// 待定价售价出库单(不传 sale_price → 0)
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 10.0, "unit_price": 5.0}},
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
orderID := extractID(w)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), token, nil)
|
||||
|
||||
var item model.StockOutItem
|
||||
db.Where("order_id = ?", orderID).First(&item)
|
||||
assert.Equal(t, float64(0), item.SalePrice)
|
||||
|
||||
// 确认售价 8
|
||||
w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-out/orders/%d/confirm-sale", orderID), token, map[string]interface{}{
|
||||
"items": []map[string]interface{}{{"item_id": item.ID, "sale_price": 8.0}},
|
||||
})
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var got model.StockOutItem
|
||||
db.First(&got, item.ID)
|
||||
assert.Equal(t, float64(8), got.SalePrice)
|
||||
var order model.StockOutOrder
|
||||
db.First(&order, orderID)
|
||||
assert.Equal(t, float64(80), order.TotalAmount) // 8 × 10
|
||||
var fr model.FinanceRecord
|
||||
require.NoError(t, db.Where("shop_id = ? AND ref_type = ?", shop.ID, "stock_out_sale_adjust").First(&fr).Error)
|
||||
assert.Equal(t, float64(80), fr.Amount)
|
||||
|
||||
// 操作员无权确认售价
|
||||
op := testutil.CreateTestUser(db, shop.ID, "op1", "pass", "operator")
|
||||
opToken := getAuthToken(op.ID, shop.ID, "operator")
|
||||
w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-out/orders/%d/confirm-sale", orderID), opToken, map[string]interface{}{
|
||||
"items": []map[string]interface{}{{"item_id": item.ID, "sale_price": 9.0}},
|
||||
})
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user