Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af56055eef | |||
| d0c71a9252 |
@@ -5,6 +5,14 @@
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.83] - 2026-07-02
|
||||
|
||||
### 修复
|
||||
- 供应商 / 客户下拉修复:现在一次返回全部往来单位,不再被截断到 20 条(此前录单和筛选的下拉最多只显示 20 个)。
|
||||
|
||||
### 改进
|
||||
- 入库录单填写的「售价」现在会写入对应商品,作为该商品的建议售价,后续出库可直接带出。
|
||||
|
||||
## [1.0.82] - 2026-07-02
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -25,7 +25,9 @@ func (h *PartnerHandler) List(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
pageSize = util.ValidatePageSize(pageSize, 20, 200)
|
||||
// 往来单位是有界的参考数据,下拉需一次取全部;放宽上限到 1000,
|
||||
// 避免前端请求大页被 ValidatePageSize 回退到默认 20(超上限即返回默认值,非截断)。
|
||||
pageSize = util.ValidatePageSize(pageSize, 20, 1000)
|
||||
|
||||
query := h.db.Model(&model.Partner{}).
|
||||
Where("shop_id = ? AND deleted_at IS NULL", shopID)
|
||||
|
||||
@@ -205,6 +205,29 @@ func TestPartnerHandler_List_KeywordSearch(t *testing.T) {
|
||||
assert.Equal(t, float64(0), resp["total"].(float64))
|
||||
}
|
||||
|
||||
// 下拉需一次取全部:请求大页(>默认 20)应返回全部,而非被 ValidatePageSize
|
||||
// 回退到默认 20(超上限即返回默认值的历史坑)。
|
||||
func TestPartnerHandler_List_LargePageSizeReturnsAll(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PT007")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
for i := 0; i < 25; i++ {
|
||||
makeRequest(r, "POST", "/api/v1/partners", token, map[string]interface{}{
|
||||
"name": fmt.Sprintf("Partner %02d", i),
|
||||
"type": "supplier",
|
||||
})
|
||||
}
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/partners?page_size=1000", token, nil)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(25), resp["total"].(float64))
|
||||
assert.Equal(t, float64(1000), resp["page_size"].(float64))
|
||||
assert.Len(t, resp["data"].([]interface{}), 25)
|
||||
}
|
||||
|
||||
func TestPartnerHandler_ShopIDFromToken(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PT007")
|
||||
|
||||
@@ -88,7 +88,7 @@ func nextProductCode(tx *gorm.DB, shopID uint64) (string, error) {
|
||||
// createIndependentProduct 为入库明细新建一个独立产品(特有产品/序列号),返回新 product。
|
||||
// "入库每行 = 一个特有产品"模型:每条明细建一个独立 product、发新序列号,不按名称复用。
|
||||
// 含 nextProductCode 自增 + 撞 uk_shop_code 唯一约束时重试。必须在事务内调用。
|
||||
func createIndependentProduct(tx *gorm.DB, shopID uint64, name, series, spec, batchNo string, prodDate *model.Date, price float64) (model.Product, error) {
|
||||
func createIndependentProduct(tx *gorm.DB, shopID uint64, name, series, spec, batchNo string, prodDate *model.Date, price, salePrice float64) (model.Product, error) {
|
||||
namePinyin, nameInitials := util.ToPinyin(name)
|
||||
var prod model.Product
|
||||
var err error
|
||||
@@ -107,6 +107,7 @@ func createIndependentProduct(tx *gorm.DB, shopID uint64, name, series, spec, ba
|
||||
BatchNo: batchNo,
|
||||
ProductionDate: prodDate,
|
||||
PurchasePrice: price,
|
||||
SalePrice: salePrice,
|
||||
NamePinyin: namePinyin,
|
||||
NameInitials: nameInitials,
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ func (h *StockInHandler) Create(c *gin.Context) {
|
||||
if it.BatchNo == "" {
|
||||
it.BatchNo = fmt.Sprintf("%s-%02d", req.OrderNo, i+1)
|
||||
}
|
||||
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice)
|
||||
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice, it.SalePrice)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func (h *StockInHandler) Update(c *gin.Context) {
|
||||
it.BatchNo = fmt.Sprintf("%s-%02d", order.OrderNo, i+1)
|
||||
}
|
||||
// 编辑草稿:旧明细已删,每条按新模型重建独立产品(旧草稿 product 无库存,暂留待后续清理)
|
||||
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice)
|
||||
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice, it.SalePrice)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -75,6 +75,44 @@ func TestStockInHandler_FullFlow(t *testing.T) {
|
||||
assert.Equal(t, float64(10), logs[0].Quantity)
|
||||
}
|
||||
|
||||
// 入库明细带 sale_price 时,为该行新建的独立产品应写入 product.SalePrice(建议售价)。
|
||||
func TestStockInHandler_Create_SalePriceToProduct(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SISALE")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main")
|
||||
token := getAuthToken(user.ID, shop.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{
|
||||
"product_name": "茅台飞天",
|
||||
"series": "53度",
|
||||
"spec": "500ml",
|
||||
"quantity": 3.0,
|
||||
"unit_price": 2680.0,
|
||||
"sale_price": 2980.0,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
orderID := extractID(w)
|
||||
|
||||
// 找到该单据明细新建的独立产品,核对进价/售价
|
||||
var item model.StockInItem
|
||||
require.NoError(t, db.Where("order_id = ?", orderID).First(&item).Error)
|
||||
require.NotZero(t, item.ProductID)
|
||||
var prod model.Product
|
||||
require.NoError(t, db.Where("id = ? AND shop_id = ?", item.ProductID, shop.ID).First(&prod).Error)
|
||||
assert.Equal(t, 2680.0, prod.PurchasePrice)
|
||||
assert.Equal(t, 2980.0, prod.SalePrice)
|
||||
// sale_price 不落 stock_in_items(gorm:"-"),仅作产品建库入参
|
||||
assert.Equal(t, "茅台飞天", prod.Name)
|
||||
}
|
||||
|
||||
func TestStockInHandler_List(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI002")
|
||||
|
||||
@@ -42,6 +42,8 @@ type StockInItem struct {
|
||||
Spec string `gorm:"size:100" json:"spec"`
|
||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
|
||||
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_price"`
|
||||
// 建议售价:仅用于入库建产品时写入 product.SalePrice,不落 stock_in_items 表(gorm:"-")。
|
||||
SalePrice float64 `gorm:"-" json:"sale_price"`
|
||||
TotalPrice float64 `gorm:"type:decimal(16,2);default:0" json:"total_price"`
|
||||
// 已退数量:0=未退,=Quantity 表示整行已退单(整行退,不做部分数量)
|
||||
ReturnedQuantity float64 `gorm:"type:decimal(12,3);default:0" json:"returned_quantity"`
|
||||
|
||||
Reference in New Issue
Block a user