Files
bj_power/bj_power_wms/internal/handler/inbound_void.go
T
SunYF 62e45b7379 feat: 完成附件存储重构与磁盘监控功能,同步物料档案与入库单规则更新
1.  新增跨平台磁盘空间监控能力,每日7点自动检测附件目录剩余空间,触发阈值告警
2.  重构附件存储方案为年/月/日/文件类型分层结构,统一MES与WMS的附件管理逻辑
3.  对齐物料档案与入库单的质量状态校验规则,仅合格品计入库存与出库
4.  实现入库单作废功能与区域库位的多级父子结构管理
5.  删除物料简称字段,补充规格型号与单位必填项,统一系统数据口径
6.  新增附件中心与磁盘状态查询接口,完善权限控制与操作日志
2026-09-19 10:54:15 +08:00

164 lines
5.2 KiB
Go

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,
})
}
}