feat(backend): 出入库编码搜索双路匹配+建单填快照;finance 趋势/日期区间;partner/product 拼音搜索
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -133,45 +133,151 @@ func TestStockOutHandler_List(t *testing.T) {
|
||||
assert.Equal(t, float64(2), resp["total"].(float64))
|
||||
}
|
||||
|
||||
// 按商品编码精确反查出库单(明细 product_code = ?)
|
||||
// 按商品编码反查出库单:快照以主数据为准(建单回填),客户端传的 product_code 被覆盖
|
||||
func TestStockOutHandler_List_FilterByProductCode(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SO_CODE")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Beer")
|
||||
find := testutil.CreateTestProduct(db, shop.ID, "ZXZFind") // code=P-ZXZFind
|
||||
other := testutil.CreateTestProduct(db, shop.ID, "OtherOne") // code=P-OtherOne
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 单1 不传 product_code(真实客户端行为);单2 传假编码,应被主数据覆盖
|
||||
makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product.ID, "product_code": "ZXZ-FIND", "quantity": 1.0},
|
||||
{"product_id": find.ID, "quantity": 1.0},
|
||||
},
|
||||
})
|
||||
makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product.ID, "product_code": "OTHER-CODE", "quantity": 1.0},
|
||||
{"product_id": other.ID, "product_code": "FAKE-CODE", "quantity": 1.0},
|
||||
},
|
||||
})
|
||||
|
||||
// 命中 1 单
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=ZXZ-FIND", token, nil)
|
||||
// 按主数据编码命中 1 单
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=P-ZXZFind", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 商品编码改为子串 LIKE(详细搜索友好):子串 ZXZ → 命中 ZXZ-FIND
|
||||
// 商品编码子串 LIKE(详细搜索友好):子串 ZXZ → 命中 P-ZXZFind
|
||||
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))
|
||||
|
||||
// 客户端传的假编码已被主数据覆盖 → 搜不到
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=FAKE-CODE", token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, float64(0), 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))
|
||||
}
|
||||
|
||||
// 建单回填快照:出库明细的编码/名称/系列/规格/批次/生产日期从商品主数据拷入
|
||||
// (修复:此前 Create/Update 不填快照 → 新建单快照全空,按编码搜不到、退单提示空名)
|
||||
func TestStockOutHandler_Create_FillsItemSnapshots(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SO_SNAP")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
prodDate := model.Date{Time: time.Date(2024, 7, 11, 0, 0, 0, 0, time.UTC)}
|
||||
product := &model.Product{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Code: "ZXZ020659",
|
||||
Name: "百富威士忌",
|
||||
Series: "12年",
|
||||
Spec: "700ml",
|
||||
BatchNo: "PZ240711",
|
||||
ProductionDate: &prodDate,
|
||||
}
|
||||
require.NoError(t, db.Create(product).Error)
|
||||
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product.ID, "quantity": 1.0},
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var item model.StockOutItem
|
||||
require.NoError(t, db.Where("shop_id = ? AND product_id = ?", shop.ID, product.ID).First(&item).Error)
|
||||
assert.Equal(t, "ZXZ020659", item.ProductCode)
|
||||
assert.Equal(t, "百富威士忌", item.ProductName)
|
||||
assert.Equal(t, "12年", item.Series)
|
||||
assert.Equal(t, "700ml", item.Spec)
|
||||
assert.Equal(t, "PZ240711", item.BatchNo)
|
||||
require.NotNil(t, item.ProductionDate)
|
||||
assert.Equal(t, "2024-07-11", item.ProductionDate.Time.Format("2006-01-02"))
|
||||
|
||||
// 引用不存在商品 → 400
|
||||
w = makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID, "order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": 99999, "quantity": 1.0},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// 存量单(快照为空、product_id 指向真实商品)按主数据编码可搜:
|
||||
// keyword 与详细搜索都走「p.code OR it.product_code」双匹配(页面能看到的就能搜到)
|
||||
func TestStockOutHandler_List_SearchByMasterCodeWithEmptySnapshot(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SO_MCODE")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
product := &model.Product{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Code: "ZXZ020659",
|
||||
Name: "百富威士忌",
|
||||
}
|
||||
require.NoError(t, db.Create(product).Error)
|
||||
|
||||
// 直造存量形态:明细快照全空串(修复前应用建单即如此)
|
||||
order := &model.StockOutOrder{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
OrderNo: "CK20260623000008",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "approved",
|
||||
OrderDate: model.Date{Time: time.Now()},
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
require.NoError(t, db.Create(&model.StockOutItem{
|
||||
OrderID: order.ID,
|
||||
ShopID: shop.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 1.0,
|
||||
}).Error)
|
||||
|
||||
// keyword 按主数据编码 → 命中(修复前为 0)
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=ZXZ020659", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 详细搜索 product_code / product 参数同样命中
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?product_code=ZXZ020659", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?product=ZXZ020659", token, nil)
|
||||
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 别的编码搜不到
|
||||
w = makeRequest(r, "GET", "/api/v1/stock-out/orders?keyword=ZXZ999999", token, nil)
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"].(float64))
|
||||
}
|
||||
|
||||
func TestStockOutHandler_List_FilterByKeyword(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SO011")
|
||||
|
||||
Reference in New Issue
Block a user