chore: release server-v1.0.74
Deploy Server / release-deploy-server (push) Successful in 2m4s

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
This commit is contained in:
wangjia
2026-06-21 22:17:24 +08:00
parent 2e941fdb5f
commit 20f9e3a410
9 changed files with 499 additions and 0 deletions
+31
View File
@@ -1,6 +1,7 @@
package handler
import (
"errors"
"fmt"
"net/http"
"strconv"
@@ -283,3 +284,33 @@ func (h *StockInHandler) Withdraw(c *gin.Context) {
Update("status", "draft")
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
}
// Return POST /api/v1/stock-in/orders/:id/return
// 入库退单:退掉指定明细,对应库存从库存删除、冲减应付。仅 approved 单、管理员/超管。
func (h *StockInHandler) Return(c *gin.Context) {
shopID := middleware.GetShopID(c)
userID := middleware.GetUserID(c)
role := middleware.GetRole(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
ItemIDs []uint64 `json:"item_ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(req.ItemIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "未选择退单明细"})
return
}
if err := h.stockSvc.ReturnStockIn(shopID, id, userID, role, req.ItemIDs); err != nil {
if errors.Is(err, service.ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "无权退单"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "returned"})
}
+31
View File
@@ -1,6 +1,7 @@
package handler
import (
"errors"
"net/http"
"strconv"
"strings"
@@ -268,3 +269,33 @@ func (h *StockOutHandler) Withdraw(c *gin.Context) {
Update("status", "draft")
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
}
// Return POST /api/v1/stock-out/orders/:id/return
// 出库退单:退掉指定明细,数量加回库存、冲减应收。仅 approved 单;管理员/超管 或 本人。
func (h *StockOutHandler) Return(c *gin.Context) {
shopID := middleware.GetShopID(c)
userID := middleware.GetUserID(c)
role := middleware.GetRole(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
var req struct {
ItemIDs []uint64 `json:"item_ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(req.ItemIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "未选择退单明细"})
return
}
if err := h.stockSvc.ReturnStockOut(shopID, id, userID, role, req.ItemIDs); err != nil {
if errors.Is(err, service.ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "无权退单"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "returned"})
}
@@ -0,0 +1,190 @@
package handler
import (
"fmt"
"net/http"
"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"
)
func TestStockReturn_StockOut_OwnAllowed(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "RET_SO")
op := testutil.CreateTestUser(db, shop.ID, "op", "pass", "operator")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
prod := testutil.CreateTestProduct(db, shop.ID, "茅台")
opToken := getAuthToken(op.ID, shop.ID, "operator")
r := setupProtectedRouter(db)
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &prod.ID, Quantity: 100})
// 操作员建出库单(本人) → 提交 → 审核
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", opToken, map[string]interface{}{
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": prod.ID, "quantity": 15.0, "unit_price": 20.0}},
})
require.Equal(t, http.StatusCreated, w.Code)
orderID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), opToken, nil)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), opToken, nil)
var item model.StockOutItem
require.NoError(t, db.Where("order_id = ?", orderID).First(&item).Error)
// 库存已扣到 85
var sum float64
db.Model(&model.Inventory{}).Where("shop_id = ? AND product_id = ? AND deleted_at IS NULL", shop.ID, prod.ID).
Select("COALESCE(SUM(quantity),0)").Scan(&sum)
require.Equal(t, float64(85), sum)
// 本人退单 → 200
w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-out/orders/%d/return", orderID), opToken,
map[string]interface{}{"item_ids": []uint64{item.ID}})
assert.Equal(t, http.StatusOK, w.Code)
// 库存加回到 100
db.Model(&model.Inventory{}).Where("shop_id = ? AND product_id = ? AND deleted_at IS NULL", shop.ID, prod.ID).
Select("COALESCE(SUM(quantity),0)").Scan(&sum)
assert.Equal(t, float64(100), sum)
// 加回是「新建一条库存行」(数量=退单数量 15)
var addBack int64
db.Model(&model.Inventory{}).
Where("shop_id = ? AND product_id = ? AND quantity = 15 AND deleted_at IS NULL", shop.ID, prod.ID).
Count(&addBack)
assert.Equal(t, int64(1), addBack, "应新建一条数量=15 的加回库存行")
// 库存流水:一条 in / stock_out_return85→100
var lg model.InventoryLog
require.NoError(t, db.Where("shop_id = ? AND ref_type = 'stock_out_return' AND direction = 'in'", shop.ID).
First(&lg).Error)
assert.Equal(t, float64(15), lg.Quantity)
assert.Equal(t, float64(85), lg.QtyBefore)
assert.Equal(t, float64(100), lg.QtyAfter)
// 明细标记已退 + 单据 return_state=full
db.First(&item, item.ID)
assert.Equal(t, item.Quantity, item.ReturnedQuantity)
var order model.StockOutOrder
db.First(&order, orderID)
assert.Equal(t, "full", order.ReturnState)
// 应收冲减:存在一条负向 receivable
var neg int64
db.Model(&model.FinanceRecord{}).
Where("shop_id = ? AND type = 'receivable' AND amount < 0 AND ref_type = 'stock_out_return'", shop.ID).
Count(&neg)
assert.Equal(t, int64(1), neg)
}
func TestStockReturn_StockIn_AdminOnly_AndRemovesInventory(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "RET_SI")
admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
op := testutil.CreateTestUser(db, shop.ID, "op", "pass", "operator")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
prod := testutil.CreateTestProduct(db, shop.ID, "五粮液")
adminToken := getAuthToken(admin.ID, shop.ID, "admin")
opToken := getAuthToken(op.ID, shop.ID, "operator")
r := setupProtectedRouter(db)
// 入库单 → 审核 → 建库存
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", adminToken, map[string]interface{}{
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": prod.ID, "quantity": 10.0, "unit_price": 50.0}},
})
require.Equal(t, http.StatusCreated, w.Code)
orderID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderID), adminToken, nil)
var item model.StockInItem
require.NoError(t, db.Where("order_id = ?", orderID).First(&item).Error)
pid := item.ProductID // 序列号模型:入库为每行新建独立 product
var sum float64
db.Model(&model.Inventory{}).Where("shop_id = ? AND product_id = ? AND deleted_at IS NULL", shop.ID, pid).
Select("COALESCE(SUM(quantity),0)").Scan(&sum)
require.Equal(t, float64(10), sum)
// 操作员退入库单 → 403
w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/return", orderID), opToken,
map[string]interface{}{"item_ids": []uint64{item.ID}})
assert.Equal(t, http.StatusForbidden, w.Code)
// 管理员退入库单 → 200,库存被删
w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/return", orderID), adminToken,
map[string]interface{}{"item_ids": []uint64{item.ID}})
assert.Equal(t, http.StatusOK, w.Code)
db.Model(&model.Inventory{}).Where("shop_id = ? AND product_id = ? AND deleted_at IS NULL", shop.ID, pid).
Select("COALESCE(SUM(quantity),0)").Scan(&sum)
assert.Equal(t, float64(0), sum)
// 入库建的那条库存行已被软删(active 中查不到)
var active int64
db.Model(&model.Inventory{}).
Where("shop_id = ? AND stock_in_item_id = ? AND deleted_at IS NULL", shop.ID, item.ID).
Count(&active)
assert.Equal(t, int64(0), active, "入库退单后对应库存行应被删除")
// 库存流水:一条 out / stock_in_return,数量=10
var lg model.InventoryLog
require.NoError(t, db.Where("shop_id = ? AND ref_type = 'stock_in_return' AND direction = 'out'", shop.ID).
First(&lg).Error)
assert.Equal(t, float64(10), lg.Quantity)
db.First(&item, item.ID)
assert.Equal(t, item.Quantity, item.ReturnedQuantity)
var order model.StockInOrder
db.First(&order, orderID)
assert.Equal(t, "full", order.ReturnState)
// 应付冲减
var neg int64
db.Model(&model.FinanceRecord{}).
Where("shop_id = ? AND type = 'payable' AND amount < 0 AND ref_type = 'stock_in_return'", shop.ID).
Count(&neg)
assert.Equal(t, int64(1), neg)
}
func TestStockReturn_StockIn_BlockedWhenSold(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "RET_BLK")
admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
prod := testutil.CreateTestProduct(db, shop.ID, "汾酒")
adminToken := getAuthToken(admin.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 入库 10 → 审核
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", adminToken, map[string]interface{}{
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": prod.ID, "quantity": 10.0, "unit_price": 50.0}},
})
inID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", inID), adminToken, nil)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", inID), adminToken, nil)
var inItem model.StockInItem
db.Where("order_id = ?", inID).First(&inItem)
// 出库 4(消耗入库新建的那个 product)→ 审核
w = makeRequest(r, "POST", "/api/v1/stock-out/orders", adminToken, map[string]interface{}{
"warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": inItem.ProductID, "quantity": 4.0, "unit_price": 80.0}},
})
outID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", outID), adminToken, nil)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", outID), adminToken, nil)
// 退入库单 → 400(库存已部分出库)
w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/return", inID), adminToken,
map[string]interface{}{"item_ids": []uint64{inItem.ID}})
assert.Equal(t, http.StatusBadRequest, w.Code)
}
@@ -64,6 +64,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
stockIn.PUT("/orders/:id/withdraw", stockInH.Withdraw)
stockIn.POST("/orders/:id/return", stockInH.Return)
// 出库路由
stockOut := api.Group("/stock-out")
@@ -74,6 +75,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
stockOut.PUT("/orders/:id/withdraw", stockOutH.Withdraw)
stockOut.POST("/orders/:id/return", stockOutH.Return)
// 库存路由
inv := api.Group("/inventory")
+8
View File
@@ -17,6 +17,8 @@ type StockInOrder struct {
OrderDate Date `gorm:"type:date" json:"order_date"`
TotalAmount float64 `gorm:"type:decimal(16,2);default:0" json:"total_amount"`
ReviewedAt *time.Time `json:"reviewed_at"`
// 退单状态:none=无 / partial=部分退单 / full=已全退(仅 approved 单可退)
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
@@ -41,6 +43,8 @@ type StockInItem struct {
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_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"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
@@ -65,6 +69,8 @@ type StockOutOrder struct {
OrderDate Date `gorm:"type:date" json:"order_date"`
TotalAmount float64 `gorm:"type:decimal(16,2);default:0" json:"total_amount"`
ReviewedAt *time.Time `json:"reviewed_at"`
// 退单状态:none / partial / full
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
@@ -89,6 +95,8 @@ type StockOutItem struct {
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_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"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
+4
View File
@@ -155,6 +155,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
// 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权)
stockIn.PUT("/orders/:id/withdraw", stockInH.Withdraw)
// 退单(已审核单退货,库存删除+冲应付):仅管理员/超管(service 内判权)
stockIn.POST("/orders/:id/return", stockInH.Return)
}
// 出库
@@ -170,6 +172,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
// 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权)
stockOut.PUT("/orders/:id/withdraw", stockOutH.Withdraw)
// 退单(已审核单退货,库存加回+冲应收):管理员/超管 或 本人(service 内判权)
stockOut.POST("/orders/:id/return", stockOutH.Return)
}
// 库存
+221
View File
@@ -13,6 +13,9 @@ import (
var ErrInsufficientStock = errors.New("insufficient stock")
// ErrForbidden 表示当前用户无权执行该操作(handler 据此返回 403)。
var ErrForbidden = errors.New("forbidden")
type StockService struct {
db *gorm.DB
}
@@ -269,6 +272,224 @@ func partnerLastBalance(tx *gorm.DB, shopID uint64, partnerID *uint64) float64 {
return last.Balance
}
// ReturnStockIn 入库单退单:把选中明细退掉,对应库存从库存中删除,并冲减应付。
// 仅 approved 单可退;权限:管理员/超管。已被出库消耗的明细禁止退(库存不足)。
func (s *StockService) ReturnStockIn(shopID, orderID, userID uint64, role string, itemIDs []uint64) error {
if role != "admin" && role != "superadmin" {
return ErrForbidden
}
return s.db.Transaction(func(tx *gorm.DB) error {
var order model.StockInOrder
if err := tx.Preload("Items").Where("id = ? AND shop_id = ?", orderID, shopID).
First(&order).Error; err != nil {
return err
}
if order.Status != "approved" {
return errors.New("只有已审核单据可退单")
}
want := idSet(itemIDs)
now := time.Now()
var returnedAmount float64
for i := range order.Items {
it := &order.Items[i]
if !want[it.ID] || it.ReturnedQuantity >= it.Quantity {
continue
}
// 找该明细审核时建的库存行(FOR UPDATE)
var inv model.Inventory
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("shop_id = ? AND stock_in_item_id = ? AND deleted_at IS NULL", shopID, it.ID).
First(&inv).Error; err != nil {
return fmt.Errorf("明细「%s」库存已出库,无法退单", itemDesc(it.ProductName, it.ProductCode))
}
// 已被部分出库(库存数量 < 入库数量)→ 禁止退
if inv.Quantity < it.Quantity {
return fmt.Errorf("明细「%s」库存已部分出库,无法退单", itemDesc(it.ProductName, it.ProductCode))
}
wid, pid := uint64(0), uint64(0)
if inv.WarehouseID != nil {
wid = *inv.WarehouseID
}
if inv.ProductID != nil {
pid = *inv.ProductID
}
// 删库存行 + 写反向流水
if err := tx.Model(&inv).Updates(map[string]interface{}{"quantity": 0, "deleted_at": now}).Error; err != nil {
return err
}
if err := tx.Create(&model.InventoryLog{
ShopID: shopID, WarehouseID: wid, ProductID: pid,
Direction: "out", Quantity: it.Quantity, QtyBefore: inv.Quantity, QtyAfter: 0,
RefType: "stock_in_return", RefID: orderID, OperatorID: &userID,
}).Error; err != nil {
return err
}
if err := tx.Model(it).Update("returned_quantity", it.Quantity).Error; err != nil {
return err
}
it.ReturnedQuantity = it.Quantity
returnedAmount += it.TotalPrice
}
// 冲减应付(负向调整记录,滚动余额)
if returnedAmount > 0 {
bal := partnerLastBalance(tx, shopID, order.PartnerID) - returnedAmount
oid := order.ID
if err := tx.Create(&model.FinanceRecord{
ShopID: shopID, PartnerID: order.PartnerID, Type: "payable",
Amount: -returnedAmount, Balance: bal, Status: "closed",
RefType: "stock_in_return", RefID: &oid, OperatorID: userID, RecordDate: now,
}).Error; err != nil {
return err
}
}
return tx.Model(&order).Update("return_state", returnStateOf(order.Items)).Error
})
}
// ReturnStockOut 出库单退单:把选中明细退掉,数量加回库存(新建库存行),并冲减应收。
// 仅 approved 单可退;权限:管理员/超管 或 本人(operator)。
func (s *StockService) ReturnStockOut(shopID, orderID, userID uint64, role string, itemIDs []uint64) error {
return s.db.Transaction(func(tx *gorm.DB) error {
var order model.StockOutOrder
if err := tx.Preload("Items.Product").Where("id = ? AND shop_id = ?", orderID, shopID).
First(&order).Error; err != nil {
return err
}
if role != "admin" && role != "superadmin" && order.OperatorID != userID {
return ErrForbidden
}
if order.Status != "approved" {
return errors.New("只有已审核单据可退单")
}
want := idSet(itemIDs)
now := time.Now()
warehouseID := order.WarehouseID
var returnedAmount float64
for i := range order.Items {
it := &order.Items[i]
if !want[it.ID] || it.ReturnedQuantity >= it.Quantity {
continue
}
productID := it.ProductID
var qtyBefore float64
tx.Model(&model.Inventory{}).
Where("shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL",
shopID, warehouseID, productID).
Select("COALESCE(SUM(quantity), 0)").Scan(&qtyBefore)
var unitPricePtr *float64
if it.UnitPrice != 0 {
up := it.UnitPrice
unitPricePtr = &up
}
unit := ""
if it.Product != nil {
unit = it.Product.Unit
}
// 加回库存:新建一条库存行
inv := model.Inventory{
ShopID: shopID, WarehouseID: &warehouseID, ProductID: &productID,
Quantity: it.Quantity,
ProductCode: it.ProductCode,
ProductName: it.ProductName,
Series: it.Series,
Spec: it.Spec,
Unit: unit,
UnitPrice: unitPricePtr,
ProductionDate: it.ProductionDate,
BatchNo: it.BatchNo,
}
if err := tx.Create(&inv).Error; err != nil {
return err
}
if err := tx.Create(&model.InventoryLog{
ShopID: shopID, WarehouseID: warehouseID, ProductID: productID,
Direction: "in", Quantity: it.Quantity, QtyBefore: qtyBefore, QtyAfter: qtyBefore + it.Quantity,
RefType: "stock_out_return", RefID: orderID, OperatorID: &userID,
}).Error; err != nil {
return err
}
if err := tx.Model(it).Update("returned_quantity", it.Quantity).Error; err != nil {
return err
}
it.ReturnedQuantity = it.Quantity
returnedAmount += it.TotalPrice
}
// 冲减应收(负向调整记录)
if returnedAmount > 0 {
bal := partnerLastBalance(tx, shopID, order.PartnerID) - returnedAmount
oid := order.ID
if err := tx.Create(&model.FinanceRecord{
ShopID: shopID, PartnerID: order.PartnerID, Type: "receivable",
Amount: -returnedAmount, Balance: bal, Status: "closed",
RefType: "stock_out_return", RefID: &oid, OperatorID: userID, RecordDate: now,
}).Error; err != nil {
return err
}
}
return tx.Model(&order).Update("return_state", returnStateOfOut(order.Items)).Error
})
}
func idSet(ids []uint64) map[uint64]bool {
m := make(map[uint64]bool, len(ids))
for _, id := range ids {
m[id] = true
}
return m
}
func itemDesc(name, code string) string {
if name == "" {
return code
}
if code == "" {
return name
}
return name + " " + code
}
// returnStateOf 据明细已退情况推导入库单退单状态。
func returnStateOf(items []model.StockInItem) string {
total, returned := 0, 0
for _, it := range items {
total++
if it.ReturnedQuantity >= it.Quantity {
returned++
}
}
switch {
case returned == 0:
return "none"
case returned >= total:
return "full"
default:
return "partial"
}
}
func returnStateOfOut(items []model.StockOutItem) string {
total, returned := 0, 0
for _, it := range items {
total++
if it.ReturnedQuantity >= it.Quantity {
returned++
}
}
switch {
case returned == 0:
return "none"
case returned >= total:
return "full"
default:
return "partial"
}
}
// GenerateOrderNo 生成单号(事务安全,FOR UPDATE 防止并发重复单号)
func (s *StockService) GenerateOrderNo(shopID uint64, orderType string) (string, error) {
var no string
+4
View File
@@ -327,6 +327,7 @@ func SetupTestDB() *gorm.DB {
order_date DATETIME,
total_amount REAL DEFAULT 0,
reviewed_at DATETIME,
return_state TEXT DEFAULT 'none',
custom_fields TEXT,
remark TEXT
)`,
@@ -346,6 +347,7 @@ func SetupTestDB() *gorm.DB {
quantity REAL NOT NULL,
unit_price REAL DEFAULT 0,
total_price REAL DEFAULT 0,
returned_quantity REAL DEFAULT 0,
batch_no TEXT,
production_date DATETIME,
custom_fields TEXT,
@@ -368,6 +370,7 @@ func SetupTestDB() *gorm.DB {
order_date DATETIME,
total_amount REAL DEFAULT 0,
reviewed_at DATETIME,
return_state TEXT DEFAULT 'none',
custom_fields TEXT,
remark TEXT
)`,
@@ -387,6 +390,7 @@ func SetupTestDB() *gorm.DB {
quantity REAL NOT NULL,
unit_price REAL DEFAULT 0,
total_price REAL DEFAULT 0,
returned_quantity REAL DEFAULT 0,
batch_no TEXT,
production_date DATETIME,
custom_fields TEXT,