1. 新增跨平台磁盘空间监控能力,每日7点自动检测附件目录剩余空间,触发阈值告警 2. 重构附件存储方案为年/月/日/文件类型分层结构,统一MES与WMS的附件管理逻辑 3. 对齐物料档案与入库单的质量状态校验规则,仅合格品计入库存与出库 4. 实现入库单作废功能与区域库位的多级父子结构管理 5. 删除物料简称字段,补充规格型号与单位必填项,统一系统数据口径 6. 新增附件中心与磁盘状态查询接口,完善权限控制与操作日志
164 lines
5.2 KiB
Go
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,
|
|
})
|
|
}
|
|
}
|