Files
bj_power/bj_power_wms/internal/handler/inbound_void.go
T

164 lines
5.2 KiB
Go
Raw Normal View History

package handler
import (
"net/http"
"strings"
"bj_power_wms/ent/inbounddetail"
"bj_power_wms/ent/inboundorder"
"bj_power_wms/ent/inventory"
"bj_power_wms/internal/svc"
)
// voidInboundHandler 入库单作废(客户诉求 2026-09-19):
// 填写作废原因 → 回滚本入库单对应的库存 → 单据标记作废(保留记录供追溯)。
//
// 回滚口径(绝不混淆):
// - 结构件(按批次):按"本单明细"的批次号扣减对应批次库存数量,扣到 0 删除该库存行。
// - 电气件(按 SN):按"本单明细"的 SN 精确删除对应 SN 库存行——只回滚本单的 SN,
// 不会波及其它入库单/物料的 SN(SN 全局唯一,一码一物)。
//
// 安全门禁:目标库存已被锁定(lockedQty>0,已被出库预留)或数量不足/已出库,
// 一律拒绝作废并提示先处理,避免把已消耗库存回滚成负数或误删他单库存。
func voidInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
InboundNo string `json:"inboundNo"`
Reason string `json:"reason"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
req.InboundNo = strings.TrimSpace(req.InboundNo)
req.Reason = strings.TrimSpace(req.Reason)
if req.InboundNo == "" {
fail(w, http.StatusBadRequest, "入库单号必填")
return
}
if req.Reason == "" {
fail(w, http.StatusBadRequest, "作废原因必填")
return
}
ib, err := ctx.EntClient.InboundOrder.Query().
Where(inboundorder.InboundNoEQ(req.InboundNo)).Only(ctx0())
if err != nil {
fail(w, http.StatusNotFound, "入库单不存在:"+req.InboundNo)
return
}
if ib.Voided {
fail(w, http.StatusConflict, "该入库单已作废,请勿重复操作")
return
}
tx, err := ctx.EntClient.Tx(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
return
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
details, err := tx.InboundDetail.Query().
Where(inbounddetail.InboundNoEQ(req.InboundNo)).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
if len(details) == 0 {
fail(w, http.StatusConflict, "该入库单无明细,无法回滚库存")
return
}
rolledSN := 0
rolledQty := 0
if ib.ManageMode == 2 {
// 电气件:逐个 SN 精确回滚(只删本单 SN 的库存行)
for _, d := range details {
if d.SnCode == "" {
continue
}
inv, e := tx.Inventory.Query().Where(inventory.SnCodeEQ(d.SnCode)).Only(ctx0())
if e != nil {
fail(w, http.StatusConflict, "SN 库存行不存在,无法作废:"+d.SnCode)
return
}
if inv.LockedQty > 0 {
fail(w, http.StatusConflict, "SN "+d.SnCode+" 已被出库锁定,请先取消出库再作废")
return
}
if inv.Status == "出库" {
fail(w, http.StatusConflict, "SN "+d.SnCode+" 已出库,无法作废(请走退库)")
return
}
if err := tx.Inventory.DeleteOneID(inv.ID).Exec(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "回滚 SN 库存失败: "+err.Error())
return
}
rolledSN++
}
} else {
// 结构件:按批次扣减本单数量
for _, d := range details {
if d.BatchNo == "" {
continue
}
inv, e := tx.Inventory.Query().
Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(d.BatchNo)).Only(ctx0())
if e != nil {
fail(w, http.StatusConflict, "批次库存行不存在,无法作废:"+d.BatchNo)
return
}
if inv.LockedQty > 0 {
fail(w, http.StatusConflict, "批次 "+d.BatchNo+" 存在锁定库存,请先取消出库再作废")
return
}
if inv.Quantity < d.Quantity {
fail(w, http.StatusConflict, "批次 "+d.BatchNo+" 在库数量不足(已被出库消耗),无法作废")
return
}
newQty := inv.Quantity - d.Quantity
if newQty <= 0 {
if err := tx.Inventory.DeleteOneID(inv.ID).Exec(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "回滚批次库存失败: "+err.Error())
return
}
} else if _, err := tx.Inventory.UpdateOneID(inv.ID).SetQuantity(newQty).Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "回滚批次库存失败: "+err.Error())
return
}
rolledQty += d.Quantity
}
}
if _, err := tx.InboundOrder.UpdateOneID(ib.ID).
SetVoided(true).SetVoidReason(req.Reason).
SetNillableVoidedBy(strPtr(r.Header.Get("X-Username"))).
SetVoidedAt(timeNow().Unix()).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "标记作废失败: "+err.Error())
return
}
if err := tx.Commit(); err != nil {
fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error())
return
}
committed = true
ctx.EventLog.Write(ctx0(), "inbound.void", r.Header.Get("X-Username"), "inbound", req.InboundNo,
"作废入库单 "+req.InboundNo+",原因:"+req.Reason, map[string]any{
"rolledSN": rolledSN, "rolledQty": rolledQty})
ok(w, map[string]any{
"inboundNo": req.InboundNo,
"rolledSN": rolledSN,
"rolledQty": rolledQty,
})
}
}