73955a139c
- inventories 加 status 枚举列(默认 stock,AutoMigrate 存量即全部「库存」)+ 索引 - 出库审核扣光置 sold 不再软删(留痕台账);退单恢复行置回 stock;盘亏仍软删 - 新接口 PUT /inventory/:id/status(仅收 stock/on_sale,已售行拒改,租户校验) - List 默认排除已售(显式 status=sold 才显示)+ status 多值筛选 - Summary:SKU/货值/数量排除已售,新增 sold_count - 公开店铺页/API 只列 on_sale——酒单从全量裸奔改为人工精选 - 测试:卖光置售/部分不改/退单回库/接口边界/跨租户/汇总口径 8 用例 - 设计方案 docs/design/inventory-sale-status.html + db-schema/CLAUDE.md 同步 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
96 lines
4.0 KiB
Go
96 lines
4.0 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/wangjia/jiu/backend/internal/model"
|
|
"github.com/wangjia/jiu/backend/testutil"
|
|
)
|
|
|
|
// 库存三态(2026-07-07):出库卖光 → sold 留痕不软删;部分出库不改状态;退单恢复 → 回 stock。
|
|
|
|
func TestApproveStockOut_SellOutMarksSold(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "STS001")
|
|
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
|
|
p := testutil.CreateTestProduct(db, shop.ID, "茅台")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
|
|
inv := model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, Quantity: 5, Status: "on_sale"}
|
|
require.NoError(t, db.Create(&inv).Error)
|
|
|
|
order := model.StockOutOrder{
|
|
TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "OUT-STS-1",
|
|
WarehouseID: wh.ID, OperatorID: user.ID, Status: "pending",
|
|
OrderDate: model.Date{Time: time.Now()},
|
|
Items: []model.StockOutItem{{ShopID: shop.ID, ProductID: p.ID, Quantity: 5}},
|
|
}
|
|
require.NoError(t, db.Create(&order).Error)
|
|
require.NoError(t, NewStockService(db).ApproveStockOut(shop.ID, order.ID, user.ID))
|
|
|
|
// 卖光:行保留(deleted_at 空)、qty=0、status=sold
|
|
var after model.Inventory
|
|
require.NoError(t, db.Unscoped().Where("id = ?", inv.ID).First(&after).Error)
|
|
assert.Equal(t, "sold", after.Status)
|
|
assert.Equal(t, float64(0), after.Quantity)
|
|
assert.Nil(t, after.DeletedAt, "卖光不再软删")
|
|
}
|
|
|
|
func TestApproveStockOut_PartialKeepsStatus(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "STS002")
|
|
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
|
|
p := testutil.CreateTestProduct(db, shop.ID, "五粮液")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
|
|
inv := model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, Quantity: 10, Status: "on_sale"}
|
|
require.NoError(t, db.Create(&inv).Error)
|
|
|
|
order := model.StockOutOrder{
|
|
TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "OUT-STS-2",
|
|
WarehouseID: wh.ID, OperatorID: user.ID, Status: "pending",
|
|
OrderDate: model.Date{Time: time.Now()},
|
|
Items: []model.StockOutItem{{ShopID: shop.ID, ProductID: p.ID, Quantity: 3}},
|
|
}
|
|
require.NoError(t, db.Create(&order).Error)
|
|
require.NoError(t, NewStockService(db).ApproveStockOut(shop.ID, order.ID, user.ID))
|
|
|
|
var after model.Inventory
|
|
require.NoError(t, db.Where("id = ?", inv.ID).First(&after).Error)
|
|
assert.Equal(t, "on_sale", after.Status, "部分出库不改状态")
|
|
assert.Equal(t, float64(7), after.Quantity)
|
|
}
|
|
|
|
func TestReturnStockOut_RestoresToStock(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "STS003")
|
|
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
|
|
p := testutil.CreateTestProduct(db, shop.ID, "剑南春")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
|
|
inv := model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, Quantity: 2, Status: "on_sale"}
|
|
require.NoError(t, db.Create(&inv).Error)
|
|
|
|
order := model.StockOutOrder{
|
|
TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "OUT-STS-3",
|
|
WarehouseID: wh.ID, OperatorID: user.ID, Status: "pending",
|
|
OrderDate: model.Date{Time: time.Now()},
|
|
Items: []model.StockOutItem{{ShopID: shop.ID, ProductID: p.ID, Quantity: 2, SalePrice: 100}},
|
|
}
|
|
require.NoError(t, db.Create(&order).Error)
|
|
svc := NewStockService(db)
|
|
require.NoError(t, svc.ApproveStockOut(shop.ID, order.ID, user.ID))
|
|
|
|
// 退单 → 新建的恢复行应为 stock(保守:需重新手动挂售)
|
|
require.NoError(t, svc.ReturnStockOut(shop.ID, order.ID, user.ID, "admin", []uint64{order.Items[0].ID}))
|
|
var restored model.Inventory
|
|
require.NoError(t, db.Where("shop_id = ? AND product_id = ? AND quantity > 0 AND deleted_at IS NULL", shop.ID, p.ID).
|
|
Order("id DESC").First(&restored).Error)
|
|
assert.Equal(t, "stock", restored.Status)
|
|
assert.Equal(t, float64(2), restored.Quantity)
|
|
}
|