Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
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.72] - 2026-06-21
|
||||
|
||||
### 新功能
|
||||
- 审核中(待审核)的入库单/出库单,管理员可「撤回」为草稿,修改后重新提交审核;已审核单据仍只读不可撤回
|
||||
|
||||
## [1.0.71] - 2026-06-21
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -255,3 +255,18 @@ func (h *StockInHandler) Reject(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "rejected"})
|
||||
}
|
||||
|
||||
// Withdraw PUT /api/v1/stock-in/orders/:id/withdraw
|
||||
// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回;
|
||||
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
||||
func (h *StockInHandler) Withdraw(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
result := h.db.Model(&model.StockInOrder{}).
|
||||
Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID).
|
||||
Update("status", "draft")
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
|
||||
}
|
||||
|
||||
@@ -182,6 +182,47 @@ func TestStockInHandler_Reject(t *testing.T) {
|
||||
assert.Equal(t, "rejected", detailData["status"])
|
||||
}
|
||||
|
||||
func TestStockInHandler_Withdraw(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI006")
|
||||
admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
||||
operator := testutil.CreateTestUser(db, shop.ID, "op", "pass", "operator")
|
||||
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, shop.ID, "Tequila")
|
||||
adminToken := getAuthToken(admin.ID, shop.ID, "admin")
|
||||
opToken := getAuthToken(operator.ID, shop.ID, "operator")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建并提交(进入 pending)
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", adminToken, map[string]interface{}{
|
||||
"warehouse_id": warehouse.ID,
|
||||
"order_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product.ID, "quantity": 5.0},
|
||||
},
|
||||
})
|
||||
orderID := extractID(w)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
|
||||
|
||||
// 1. 非管理员撤回 → 403
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), opToken, nil)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
|
||||
// 2. 管理员撤回 → 200,状态回到 draft
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), adminToken, nil)
|
||||
assert.Equal(t, "draft", parseResponse(w)["data"].(map[string]interface{})["status"])
|
||||
|
||||
// 3. 撤回后已是 draft,再撤回 → 400(仅 pending 可撤回)
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
// 4. 撤回为 draft 后可再次修改并提交
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestStockInHandler_GetNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "SI006")
|
||||
|
||||
@@ -240,3 +240,18 @@ func (h *StockOutHandler) Reject(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "rejected"})
|
||||
}
|
||||
|
||||
// Withdraw PUT /api/v1/stock-out/orders/:id/withdraw
|
||||
// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回;
|
||||
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
||||
func (h *StockOutHandler) Withdraw(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
result := h.db.Model(&model.StockOutOrder{}).
|
||||
Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID).
|
||||
Update("status", "draft")
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
|
||||
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
||||
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
||||
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
||||
stockIn.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockInH.Withdraw)
|
||||
|
||||
// 出库路由
|
||||
stockOut := api.Group("/stock-out")
|
||||
@@ -72,6 +73,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
|
||||
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
|
||||
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
||||
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
||||
stockOut.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockOutH.Withdraw)
|
||||
|
||||
// 库存路由
|
||||
inv := api.Group("/inventory")
|
||||
|
||||
@@ -153,6 +153,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
||||
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
||||
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
||||
// 撤回(审核中→草稿)仅管理员
|
||||
stockIn.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockInH.Withdraw)
|
||||
}
|
||||
|
||||
// 出库
|
||||
@@ -166,6 +168,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
|
||||
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
||||
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
||||
// 撤回(审核中→草稿)仅管理员
|
||||
stockOut.PUT("/orders/:id/withdraw", middleware.AdminOnly(), stockOutH.Withdraw)
|
||||
}
|
||||
|
||||
// 库存
|
||||
|
||||
Reference in New Issue
Block a user