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:
@@ -101,6 +101,94 @@ func TestStockInHandler_List(t *testing.T) {
|
||||
assert.Equal(t, float64(2), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestStockInHandler_Summary(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SISUM")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
wh := testutil.CreateTestWarehouse(db, shop.ID, "W")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": wh.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_name": "A", "quantity": 2.0, "unit_price": 100.0}},
|
||||
})
|
||||
}
|
||||
var first model.StockInOrder
|
||||
db.Where("shop_id = ?", shop.ID).Order("id ASC").First(&first)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", first.ID), token, nil)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-in/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(400), s["month_amount"])
|
||||
assert.Equal(t, float64(1), s["pending_count"])
|
||||
}
|
||||
|
||||
func TestStockInHandler_DetailFilter(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SIDF")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
wh := testutil.CreateTestWarehouse(db, shop.ID, "W")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_name": "BlueWine", "series": "Blue", "spec": "500ml", "quantity": 1.0, "unit_price": 10.0}},
|
||||
})
|
||||
makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{{"product_name": "PlainWine", "series": "Plain", "spec": "480ml", "quantity": 1.0, "unit_price": 10.0}},
|
||||
})
|
||||
|
||||
// 系列过滤(JOIN 明细)
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-in/orders?series=Blue", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"])
|
||||
// 商品信息过滤(明细快照名 LIKE)
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?product=Plain", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"])
|
||||
// 规格过滤
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?spec=500ml", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"])
|
||||
// 组合:系列 Blue + 规格 480ml → 无交集
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?series=Blue&spec=480ml", token, nil)
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"])
|
||||
|
||||
// 商品编码过滤(明细 product_code LIKE):给 BlueWine 明细设已知码再按子串筛
|
||||
db.Model(&model.StockInItem{}).
|
||||
Where("shop_id = ? AND product_name = ?", shop.ID, "BlueWine").
|
||||
Update("product_code", "PC-XYZ")
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?product_code=XYZ", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"])
|
||||
|
||||
// 同一明细语义:一张单两条明细,系列在明细1、规格分属明细2。
|
||||
makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_name": "MixA", "series": "MixS", "spec": "111", "quantity": 1.0, "unit_price": 10.0},
|
||||
{"product_name": "MixB", "series": "OtherS", "spec": "222", "quantity": 1.0, "unit_price": 10.0},
|
||||
},
|
||||
})
|
||||
// 跨明细组合(系列在明细1、规格在明细2)→ 同一明细语义下无单命中
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?series=MixS&spec=222", token, nil)
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"])
|
||||
// 同一明细命中(系列 MixS + 规格 111 均在明细1)→ 1
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?series=MixS&spec=111", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"])
|
||||
|
||||
// 退单状态过滤:把一单标 partial 再按 return_state 筛
|
||||
var one model.StockInOrder
|
||||
db.Where("shop_id = ?", shop.ID).First(&one)
|
||||
db.Model(&model.StockInOrder{}).Where("id = ?", one.ID).Update("return_state", "partial")
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?return_state=partial", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"])
|
||||
}
|
||||
|
||||
func TestStockInHandler_Submit_WrongStatus(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI003")
|
||||
@@ -438,9 +526,10 @@ func TestStockInHandler_List_FilterByKeyword(t *testing.T) {
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
// 按往来单位名搜索 → 只命中单据1
|
||||
// 往来单位名不再进入 keyword 范围(供应商/客户改由工具栏下拉筛选)→ 搜不到
|
||||
// (商品是 Brandy,不含「茅台」)
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword=茅台", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 按单号搜索 → 只命中单据1
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword="+order1No, token, nil)
|
||||
|
||||
Reference in New Issue
Block a user