fix(test): 修复测试文件类型错误和测试数据库 schema 过期问题
- 修正 stock_in/out 测试中 WarehouseID/ProductID 指针误用(StockOrder 用 uint64,Inventory 用 *uint64) - 更新 testutil/setup.go:inventories 表加入所有批次字段,stock_in_items 加 production_date,新增 finance_records 表 - 修正 inventory handler 中 DATE_FORMAT(MySQL 专属)→ DATE()(跨 DB 兼容) - 更新断言:order_no 前缀改为 RK,库存总量用 SUM 替代 First - 库存 List 新增 in_stock=1 过滤器 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,7 @@ func (h *InventoryHandler) List(c *gin.Context) {
|
|||||||
|
|
||||||
keyword := c.Query("keyword")
|
keyword := c.Query("keyword")
|
||||||
warehouseIDStr := c.Query("warehouse_id")
|
warehouseIDStr := c.Query("warehouse_id")
|
||||||
|
inStock := c.Query("in_stock")
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
baseWhere += " AND (COALESCE(NULLIF(p.name,''), inv.product_name) LIKE ? OR COALESCE(NULLIF(p.code,''), inv.product_code) LIKE ?)"
|
baseWhere += " AND (COALESCE(NULLIF(p.name,''), inv.product_name) LIKE ? OR COALESCE(NULLIF(p.code,''), inv.product_code) LIKE ?)"
|
||||||
@@ -71,6 +72,9 @@ func (h *InventoryHandler) List(c *gin.Context) {
|
|||||||
baseWhere += " AND inv.warehouse_id = ?"
|
baseWhere += " AND inv.warehouse_id = ?"
|
||||||
args = append(args, warehouseIDStr)
|
args = append(args, warehouseIDStr)
|
||||||
}
|
}
|
||||||
|
if inStock == "1" {
|
||||||
|
baseWhere += " AND inv.quantity > 0"
|
||||||
|
}
|
||||||
|
|
||||||
// Count query
|
// Count query
|
||||||
countSQL := `
|
countSQL := `
|
||||||
@@ -96,13 +100,13 @@ func (h *InventoryHandler) List(c *gin.Context) {
|
|||||||
COALESCE(NULLIF(p.unit,''), inv.unit, '') AS unit,
|
COALESCE(NULLIF(p.unit,''), inv.unit, '') AS unit,
|
||||||
COALESCE(NULLIF(w.name,''), inv.warehouse_name, '') AS warehouse_name,
|
COALESCE(NULLIF(w.name,''), inv.warehouse_name, '') AS warehouse_name,
|
||||||
COALESCE(sii.unit_price, inv.unit_price) AS unit_price,
|
COALESCE(sii.unit_price, inv.unit_price) AS unit_price,
|
||||||
COALESCE(DATE_FORMAT(sii.production_date,'%Y-%m-%d'), DATE_FORMAT(inv.production_date,'%Y-%m-%d')) AS production_date,
|
COALESCE(DATE(sii.production_date), DATE(inv.production_date)) AS production_date,
|
||||||
COALESCE(NULLIF(sii.batch_no,''), inv.batch_no, '') AS batch_no,
|
COALESCE(NULLIF(sii.batch_no,''), inv.batch_no, '') AS batch_no,
|
||||||
inv.supplier_name,
|
inv.supplier_name,
|
||||||
inv.remark,
|
inv.remark,
|
||||||
COALESCE(p.brand, '') AS brand,
|
COALESCE(p.brand, '') AS brand,
|
||||||
p.min_stock,
|
p.min_stock,
|
||||||
DATE_FORMAT(inv.created_at, '%Y-%m-%dT%H:%i:%sZ') AS created_at
|
inv.created_at
|
||||||
FROM inventories inv
|
FROM inventories inv
|
||||||
LEFT JOIN stock_in_items sii ON sii.id = inv.stock_in_item_id
|
LEFT JOIN stock_in_items sii ON sii.id = inv.stock_in_item_id
|
||||||
LEFT JOIN products p ON p.id = inv.product_id
|
LEFT JOIN products p ON p.id = inv.product_id
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func TestInventoryHandler_List(t *testing.T) {
|
|||||||
// 直接插入库存记录
|
// 直接插入库存记录
|
||||||
inv := model.Inventory{
|
inv := model.Inventory{
|
||||||
ShopID: shop.ID,
|
ShopID: shop.ID,
|
||||||
WarehouseID: warehouse.ID,
|
WarehouseID: &warehouse.ID,
|
||||||
ProductID: product.ID,
|
ProductID: &product.ID,
|
||||||
Quantity: 100,
|
Quantity: 100,
|
||||||
}
|
}
|
||||||
require.NoError(t, db.Create(&inv).Error)
|
require.NoError(t, db.Create(&inv).Error)
|
||||||
@@ -49,8 +49,8 @@ func TestInventoryHandler_List_FilterByWarehouse(t *testing.T) {
|
|||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
// 两个仓库各有一个库存
|
// 两个仓库各有一个库存
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse1.ID, ProductID: product1.ID, Quantity: 10})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse1.ID, ProductID: &product1.ID, Quantity: 10})
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse2.ID, ProductID: product2.ID, Quantity: 20})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse2.ID, ProductID: &product2.ID, Quantity: 20})
|
||||||
|
|
||||||
// 按仓库过滤
|
// 按仓库过滤
|
||||||
w := makeRequest(r, "GET", fmt.Sprintf("/api/v1/inventory?warehouse_id=%d", warehouse1.ID), token, nil)
|
w := makeRequest(r, "GET", fmt.Sprintf("/api/v1/inventory?warehouse_id=%d", warehouse1.ID), token, nil)
|
||||||
@@ -69,8 +69,8 @@ func TestInventoryHandler_List_InStockOnly(t *testing.T) {
|
|||||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse.ID, ProductID: product1.ID, Quantity: 10})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product1.ID, Quantity: 10})
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse.ID, ProductID: product2.ID, Quantity: 0})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product2.ID, Quantity: 0})
|
||||||
|
|
||||||
// 只显示有库存的
|
// 只显示有库存的
|
||||||
w := makeRequest(r, "GET", "/api/v1/inventory?in_stock=1", token, nil)
|
w := makeRequest(r, "GET", "/api/v1/inventory?in_stock=1", token, nil)
|
||||||
@@ -121,8 +121,8 @@ func TestInventoryHandler_CreateCheck(t *testing.T) {
|
|||||||
// 先创建库存
|
// 先创建库存
|
||||||
db.Create(&model.Inventory{
|
db.Create(&model.Inventory{
|
||||||
ShopID: shop.ID,
|
ShopID: shop.ID,
|
||||||
WarehouseID: warehouse.ID,
|
WarehouseID: &warehouse.ID,
|
||||||
ProductID: product.ID,
|
ProductID: &product.ID,
|
||||||
Quantity: 50,
|
Quantity: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ func TestInventoryHandler_List_HotelIsolation(t *testing.T) {
|
|||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
// 酒店 A 有库存
|
// 酒店 A 有库存
|
||||||
db.Create(&model.Inventory{ShopID: shopA.ID, WarehouseID: warehouseA.ID, ProductID: productA.ID, Quantity: 100})
|
db.Create(&model.Inventory{ShopID: shopA.ID, WarehouseID: &warehouseA.ID, ProductID: &productA.ID, Quantity: 100})
|
||||||
|
|
||||||
// 酒店 A 能看到自己的库存
|
// 酒店 A 能看到自己的库存
|
||||||
w := makeRequest(r, "GET", "/api/v1/inventory", tokenA, nil)
|
w := makeRequest(r, "GET", "/api/v1/inventory", tokenA, nil)
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func TestStockOutHandler_FullFlow(t *testing.T) {
|
|||||||
// 先建立库存
|
// 先建立库存
|
||||||
db.Create(&model.Inventory{
|
db.Create(&model.Inventory{
|
||||||
ShopID: shop.ID,
|
ShopID: shop.ID,
|
||||||
WarehouseID: warehouse.ID,
|
WarehouseID: &warehouse.ID,
|
||||||
ProductID: product.ID,
|
ProductID: &product.ID,
|
||||||
Quantity: 100,
|
Quantity: 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ func TestStockOutHandler_InsufficientStock(t *testing.T) {
|
|||||||
// 库存只有 5
|
// 库存只有 5
|
||||||
db.Create(&model.Inventory{
|
db.Create(&model.Inventory{
|
||||||
ShopID: shop.ID,
|
ShopID: shop.ID,
|
||||||
WarehouseID: warehouse.ID,
|
WarehouseID: &warehouse.ID,
|
||||||
ProductID: product.ID,
|
ProductID: &product.ID,
|
||||||
Quantity: 5,
|
Quantity: 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func TestStockOutHandler_List(t *testing.T) {
|
|||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
// 先建立库存
|
// 先建立库存
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse.ID, ProductID: product.ID, Quantity: 100})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 100})
|
||||||
|
|
||||||
// 创建 2 个出库单
|
// 创建 2 个出库单
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
@@ -142,7 +142,7 @@ func TestStockOutHandler_Reject(t *testing.T) {
|
|||||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse.ID, ProductID: product.ID, Quantity: 100})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 100})
|
||||||
|
|
||||||
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||||
"warehouse_id": warehouse.ID,
|
"warehouse_id": warehouse.ID,
|
||||||
@@ -206,8 +206,8 @@ func TestStockOutHandler_TenantIsolation(t *testing.T) {
|
|||||||
// 建立门店 A 的库存
|
// 建立门店 A 的库存
|
||||||
db.Create(&model.Inventory{
|
db.Create(&model.Inventory{
|
||||||
ShopID: shopA.ID,
|
ShopID: shopA.ID,
|
||||||
WarehouseID: warehouseA.ID,
|
WarehouseID: &warehouseA.ID,
|
||||||
ProductID: productA.ID,
|
ProductID: &productA.ID,
|
||||||
Quantity: 50,
|
Quantity: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ func TestStockOutHandler_Approve_NotPending(t *testing.T) {
|
|||||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse.ID, ProductID: product.ID, Quantity: 50})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 50})
|
||||||
|
|
||||||
// 创建但不提交
|
// 创建但不提交
|
||||||
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||||
@@ -285,7 +285,7 @@ func TestStockOutHandler_InventoryLog_OnApprove(t *testing.T) {
|
|||||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|
||||||
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: warehouse.ID, ProductID: product.ID, Quantity: 100})
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 100})
|
||||||
|
|
||||||
// 创建、提交、审核
|
// 创建、提交、审核
|
||||||
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ func TestStockService_ApproveStockOut_Success(t *testing.T) {
|
|||||||
// 先入库
|
// 先入库
|
||||||
inv := model.Inventory{
|
inv := model.Inventory{
|
||||||
ShopID: shop.ID,
|
ShopID: shop.ID,
|
||||||
WarehouseID: warehouse.ID,
|
WarehouseID: &warehouse.ID,
|
||||||
ProductID: product.ID,
|
ProductID: &product.ID,
|
||||||
Quantity: 20,
|
Quantity: 20,
|
||||||
}
|
}
|
||||||
require.NoError(t, db.Create(&inv).Error)
|
require.NoError(t, db.Create(&inv).Error)
|
||||||
@@ -149,8 +149,8 @@ func TestStockService_ApproveStockOut_InsufficientStock(t *testing.T) {
|
|||||||
// 库存只有 3
|
// 库存只有 3
|
||||||
inv := model.Inventory{
|
inv := model.Inventory{
|
||||||
ShopID: shop.ID,
|
ShopID: shop.ID,
|
||||||
WarehouseID: warehouse.ID,
|
WarehouseID: &warehouse.ID,
|
||||||
ProductID: product.ID,
|
ProductID: &product.ID,
|
||||||
Quantity: 3,
|
Quantity: 3,
|
||||||
}
|
}
|
||||||
require.NoError(t, db.Create(&inv).Error)
|
require.NoError(t, db.Create(&inv).Error)
|
||||||
@@ -207,7 +207,7 @@ func TestStockService_ApproveStockOut_ProductNotInInventory(t *testing.T) {
|
|||||||
svc := NewStockService(db)
|
svc := NewStockService(db)
|
||||||
err := svc.ApproveStockOut(shop.ID, order.ID, user.ID)
|
err := svc.ApproveStockOut(shop.ID, order.ID, user.ID)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "not in inventory")
|
assert.ErrorIs(t, err, ErrInsufficientStock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStockService_GenerateOrderNo(t *testing.T) {
|
func TestStockService_GenerateOrderNo(t *testing.T) {
|
||||||
@@ -219,7 +219,7 @@ func TestStockService_GenerateOrderNo(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, no)
|
assert.NotEmpty(t, no)
|
||||||
// 单号格式: prefix + date + 6位序号
|
// 单号格式: prefix + date + 6位序号
|
||||||
assert.Contains(t, no, "st")
|
assert.Contains(t, no, "RK")
|
||||||
|
|
||||||
// 第二次生成序号应递增
|
// 第二次生成序号应递增
|
||||||
no2, err := svc.GenerateOrderNo(shop.ID, "stock_in")
|
no2, err := svc.GenerateOrderNo(shop.ID, "stock_in")
|
||||||
@@ -288,9 +288,11 @@ func TestStockService_MultipleStockIn_AccumulatesInventory(t *testing.T) {
|
|||||||
require.NoError(t, db.Create(&order2).Error)
|
require.NoError(t, db.Create(&order2).Error)
|
||||||
require.NoError(t, svc.ApproveStockIn(shop.ID, order2.ID, user.ID))
|
require.NoError(t, svc.ApproveStockIn(shop.ID, order2.ID, user.ID))
|
||||||
|
|
||||||
// 总库存应该是 15
|
// 总库存应该是 15(两个批次之和)
|
||||||
var inv model.Inventory
|
var totalQty float64
|
||||||
db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?",
|
db.Model(&model.Inventory{}).
|
||||||
shop.ID, warehouse.ID, product.ID).First(&inv)
|
Where("shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL",
|
||||||
assert.Equal(t, float64(15), inv.Quantity)
|
shop.ID, &warehouse.ID, &product.ID).
|
||||||
|
Select("COALESCE(SUM(quantity), 0)").Scan(&totalQty)
|
||||||
|
assert.Equal(t, float64(15), totalQty)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ func SetupTestDB() *gorm.DB {
|
|||||||
unit_price REAL DEFAULT 0,
|
unit_price REAL DEFAULT 0,
|
||||||
total_price REAL DEFAULT 0,
|
total_price REAL DEFAULT 0,
|
||||||
batch_no TEXT,
|
batch_no TEXT,
|
||||||
|
production_date DATETIME,
|
||||||
custom_fields TEXT,
|
custom_fields TEXT,
|
||||||
remark TEXT
|
remark TEXT
|
||||||
)`,
|
)`,
|
||||||
@@ -223,11 +224,25 @@ func SetupTestDB() *gorm.DB {
|
|||||||
`CREATE TABLE IF NOT EXISTS inventories (
|
`CREATE TABLE IF NOT EXISTS inventories (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
shop_id INTEGER NOT NULL,
|
shop_id INTEGER NOT NULL,
|
||||||
warehouse_id INTEGER NOT NULL,
|
warehouse_id INTEGER,
|
||||||
product_id INTEGER NOT NULL,
|
product_id INTEGER,
|
||||||
|
stock_in_item_id INTEGER,
|
||||||
|
inventory_check_id INTEGER,
|
||||||
quantity REAL DEFAULT 0,
|
quantity REAL DEFAULT 0,
|
||||||
|
product_code TEXT DEFAULT '',
|
||||||
|
product_name TEXT DEFAULT '',
|
||||||
|
series TEXT DEFAULT '',
|
||||||
|
spec TEXT DEFAULT '',
|
||||||
|
unit TEXT DEFAULT '',
|
||||||
|
warehouse_name TEXT DEFAULT '',
|
||||||
|
unit_price REAL,
|
||||||
|
production_date DATETIME,
|
||||||
|
batch_no TEXT DEFAULT '',
|
||||||
|
supplier_name TEXT DEFAULT '',
|
||||||
|
remark TEXT DEFAULT '',
|
||||||
|
created_at DATETIME,
|
||||||
updated_at DATETIME,
|
updated_at DATETIME,
|
||||||
UNIQUE(shop_id, warehouse_id, product_id)
|
deleted_at DATETIME
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE IF NOT EXISTS inventory_logs (
|
`CREATE TABLE IF NOT EXISTS inventory_logs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -269,6 +284,24 @@ func SetupTestDB() *gorm.DB {
|
|||||||
actual_qty REAL,
|
actual_qty REAL,
|
||||||
remark TEXT
|
remark TEXT
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS finance_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
deleted_at DATETIME,
|
||||||
|
shop_id INTEGER NOT NULL,
|
||||||
|
partner_id INTEGER,
|
||||||
|
type TEXT,
|
||||||
|
amount REAL DEFAULT 0,
|
||||||
|
balance REAL DEFAULT 0,
|
||||||
|
status TEXT DEFAULT 'open',
|
||||||
|
ref_type TEXT,
|
||||||
|
ref_id INTEGER,
|
||||||
|
operator_id INTEGER,
|
||||||
|
record_date DATETIME,
|
||||||
|
remark TEXT,
|
||||||
|
custom_fields TEXT
|
||||||
|
)`,
|
||||||
`CREATE TABLE IF NOT EXISTS number_rules (
|
`CREATE TABLE IF NOT EXISTS number_rules (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
shop_id INTEGER NOT NULL,
|
shop_id INTEGER NOT NULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user