feat: 完成WMS系统权限体系与业务功能迭代
本提交完成了WMS系统的多维度优化升级: 1. 新增RBAC权限系统,支持角色/菜单/按钮三级权限管控 2. 重构库存盘点、物料管理、区域维护等模块的查询筛选与展示逻辑 3. 优化入库/出库/质检等业务流程,完善数据冗余与业务闭环 4. 移除旧装箱表,将装箱逻辑合并到出库主表 5. 新增前端权限判断工具、导出工具与端到端测试用例 6. 补充完善各类注释与数据库字段说明
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
"bj_power_wms/ent/inventory"
|
||||
@@ -60,7 +62,7 @@ func deductStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "工单物料台账不存在,请先同步工单 BOM: "+req.OrderNo+"/"+req.MaterialCode)
|
||||
fail(w, http.StatusBadRequest, "工单物料台账不存在,请先在 MES 同步工单 BOM 后再领料;如无需挂靠工单(退料/样品/报废/发货),请改用「通用出库」: "+req.OrderNo+"/"+req.MaterialCode)
|
||||
return
|
||||
}
|
||||
if ledger.Status == "领料完结" {
|
||||
@@ -237,6 +239,7 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
orderNo := r.URL.Query().Get("orderNo")
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
outboundNo := r.URL.Query().Get("outboundNo")
|
||||
boxNo := r.URL.Query().Get("boxNo")
|
||||
|
||||
q := ctx.EntClient.OutboundOrder.Query()
|
||||
if orderNo != "" {
|
||||
@@ -248,13 +251,16 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
if outboundNo != "" {
|
||||
q = q.Where(outboundorder.OutboundNoEQ(outboundNo))
|
||||
}
|
||||
if boxNo != "" {
|
||||
q = q.Where(outboundorder.BoxNoContains(boxNo))
|
||||
}
|
||||
|
||||
total, err := q.Count(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("id")).
|
||||
list, err := q.Order(ent.Desc("created_at")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -283,6 +289,259 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// generalOutboundRequest 通用出库请求(不依赖 MES 工单)
|
||||
type generalOutboundRequest struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
BatchNo string `json:"batchNo"`
|
||||
SnList []string `json:"snList"`
|
||||
Qty int `json:"qty"`
|
||||
Operator string `json:"operator"`
|
||||
ZoneCode string `json:"zoneCode"`
|
||||
BoxNo string `json:"boxNo"`
|
||||
ContractNo string `json:"contractNo"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// generalOutboundHandler 通用出库(手动出库,不依赖工单)
|
||||
// 适用:退料 / 样品 / 报废 / 发货 等不挂靠 MES 工单的场景。
|
||||
// 支持结构件批次与精密件 SN;填箱号时一并把装箱信息(箱号/SN清单/合同号)
|
||||
// 落到统一的出库主表,不再另建装箱表。
|
||||
// 事务保证:出库单 + 库存扣减 + 出库明细 原子提交;库存用条件原子更新避免负库存。
|
||||
func generalOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req generalOutboundRequest
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if req.MaterialCode == "" {
|
||||
fail(w, http.StatusBadRequest, "materialCode 必填")
|
||||
return
|
||||
}
|
||||
if req.BatchNo == "" && len(req.SnList) == 0 {
|
||||
fail(w, http.StatusBadRequest, "batchNo 或 snList 至少提供一个")
|
||||
return
|
||||
}
|
||||
|
||||
manageMode := 1
|
||||
if len(req.SnList) > 0 {
|
||||
manageMode = 2
|
||||
}
|
||||
// SN 出库数量以列表长度计
|
||||
actualQty := req.Qty
|
||||
if manageMode == 2 {
|
||||
actualQty = len(req.SnList)
|
||||
}
|
||||
if actualQty <= 0 {
|
||||
fail(w, http.StatusBadRequest, "数量必须大于 0")
|
||||
return
|
||||
}
|
||||
|
||||
outboundNo := "OB" + uuid.NewString()[:12]
|
||||
|
||||
tx, err := ctx.EntClient.Tx(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
ob, err := tx.OutboundOrder.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetOutboundType("general").
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetManageMode(manageMode).
|
||||
SetNillableBatchNo(strPtr(req.BatchNo)).
|
||||
SetNillableZoneCode(strPtr(req.ZoneCode)).
|
||||
SetQuantity(0).
|
||||
SetNillableOperator(strPtr(req.Operator)).
|
||||
SetNillableRemark(strPtr(req.Remark)).
|
||||
SetNillableBoxNo(strPtr(req.BoxNo)).
|
||||
SetNillableContractNo(strPtr(req.ContractNo)).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建出库单失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 批次扣减(结构件)
|
||||
if req.BatchNo != "" {
|
||||
b, err := tx.Inventory.Query().
|
||||
Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(req.BatchNo)).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "批次不存在: "+req.BatchNo)
|
||||
return
|
||||
}
|
||||
avail := b.Quantity - b.LockedQty
|
||||
if avail < actualQty {
|
||||
fail(w, http.StatusConflict, "批次库存不足: "+req.BatchNo+" 可用 "+itoa(avail))
|
||||
return
|
||||
}
|
||||
aff, err := tx.Inventory.Update().
|
||||
Where(inventory.ID(b.ID), inventory.QuantityGTE(actualQty)).
|
||||
AddQuantity(-actualQty).
|
||||
AddLockedQty(-min(actualQty, b.LockedQty)).
|
||||
Save(ctx0())
|
||||
if err != nil || aff == 0 {
|
||||
fail(w, http.StatusConflict, "批次扣减失败(库存不足或并发冲突): "+req.BatchNo)
|
||||
return
|
||||
}
|
||||
if _, err = tx.OutboundDetail.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetBatchNo(req.BatchNo).
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetQuantity(actualQty).
|
||||
Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建出库明细失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SN 扣减(精密件):仅在库/锁定的 SN 可出库
|
||||
if len(req.SnList) > 0 {
|
||||
for _, sn := range req.SnList {
|
||||
aff, err := tx.Inventory.Update().
|
||||
Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(sn), inventory.StatusIn("在库", "锁定")).
|
||||
SetStatus("出库").
|
||||
SetNillableZoneCode(strPtr(req.ZoneCode)).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "SN 出库失败: "+sn+" "+err.Error())
|
||||
return
|
||||
}
|
||||
if aff == 0 {
|
||||
fail(w, http.StatusBadRequest, "SN 不可出库(不存在或非在库/锁定): "+sn)
|
||||
return
|
||||
}
|
||||
if _, err = tx.OutboundDetail.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetSnCode(sn).
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetQuantity(1).
|
||||
Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建出库明细失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回写实际数量 + 装箱 SN 清单(JSON)
|
||||
upd := tx.OutboundOrder.UpdateOneID(ob.ID).SetQuantity(actualQty)
|
||||
if len(req.SnList) > 0 && req.BoxNo != "" {
|
||||
upd.SetBoxSnList(snListJSON(req.SnList))
|
||||
}
|
||||
if _, err = upd.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
|
||||
|
||||
logx.Infof("general outbound: material=%s qty=%d box=%s", req.MaterialCode, actualQty, req.BoxNo)
|
||||
ok(w, map[string]any{
|
||||
"outboundNo": outboundNo,
|
||||
"qty": actualQty,
|
||||
"boxNo": req.BoxNo,
|
||||
"type": "general",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// snListJSON 把 SN 列表序列化为 JSON 数组字符串
|
||||
func snListJSON(sn []string) string {
|
||||
b, err := json.Marshal(sn)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// exportOutboundHandler 统一主表全部出库记录导出
|
||||
// 不依赖前端分页,一次返回全部(可按类型/工单/物料/箱号过滤),便于整表导出。
|
||||
func exportOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
outboundType := r.URL.Query().Get("outboundType")
|
||||
orderNo := r.URL.Query().Get("orderNo")
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
boxNo := r.URL.Query().Get("boxNo")
|
||||
|
||||
q := ctx.EntClient.OutboundOrder.Query()
|
||||
if outboundType != "" {
|
||||
q = q.Where(outboundorder.OutboundTypeEQ(outboundType))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(outboundorder.OrderNoEQ(orderNo))
|
||||
}
|
||||
if materialCode != "" {
|
||||
q = q.Where(outboundorder.MaterialCodeEQ(materialCode))
|
||||
}
|
||||
if boxNo != "" {
|
||||
q = q.Where(outboundorder.BoxNoContains(boxNo))
|
||||
}
|
||||
|
||||
list, err := q.Order(ent.Desc("created_at")).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
type exportRow struct {
|
||||
*ent.OutboundOrder
|
||||
SnCodes string `json:"snCodes"`
|
||||
Details []*ent.OutboundDetail `json:"details"`
|
||||
}
|
||||
rows := make([]exportRow, 0, len(list))
|
||||
for _, ob := range list {
|
||||
details, _ := ctx.EntClient.OutboundDetail.Query().
|
||||
Where(outbounddetail.OutboundNoEQ(ob.OutboundNo)).
|
||||
All(ctx0())
|
||||
er := exportRow{OutboundOrder: ob, SnCodes: "", Details: details}
|
||||
sns := make([]string, 0, len(details))
|
||||
for _, d := range details {
|
||||
if d.SnCode != "" {
|
||||
sns = append(sns, d.SnCode)
|
||||
}
|
||||
}
|
||||
if len(sns) > 0 {
|
||||
er.SnCodes = strings.Join(sns, ",")
|
||||
}
|
||||
rows = append(rows, er)
|
||||
}
|
||||
|
||||
// xlsx 导出
|
||||
if r.URL.Query().Get("format") == "xlsx" {
|
||||
headers := []string{"出库单号", "类型", "工单号", "物料编码", "物料名称", "数量", "装箱号", "合同号", "SN清单", "创建时间"}
|
||||
matrix := make([][]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ob := row.OutboundOrder
|
||||
tn := "通用出库"
|
||||
if ob.OutboundType == "workorder" {
|
||||
tn = "备料出库"
|
||||
}
|
||||
matrix = append(matrix, []any{
|
||||
ob.OutboundNo, tn, ob.OrderNo, ob.MaterialCode, ob.MaterialName,
|
||||
ob.Quantity, ob.BoxNo, ob.ContractNo, row.SnCodes,
|
||||
unixFmt(ob.CreatedAt),
|
||||
})
|
||||
}
|
||||
sendExcel(w, xlsxFilename("出库管理"), headers, matrix)
|
||||
return
|
||||
}
|
||||
|
||||
ok(w, map[string]any{"total": len(rows), "list": rows})
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
Reference in New Issue
Block a user