feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段

- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
This commit is contained in:
SunYF
2026-09-17 12:49:07 +08:00
parent b4b274301d
commit 1cc93795ce
191 changed files with 24504 additions and 1250 deletions
+119 -15
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"strings"
"time"
"bj_power_wms/ent"
"bj_power_wms/ent/inventory"
@@ -211,10 +212,14 @@ func deductStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if newOutQty >= ledgerNow.TotalQty {
status = "领料完结"
}
if _, err = tx.OrderMaterialLedger.UpdateOneID(ledgerNow.ID).
// 台账 completed_at = 最后一笔领料完结出库时间(客户诉求 问题记录 L238-240:台账增加完成时间筛选)
ledgerUpd := tx.OrderMaterialLedger.UpdateOneID(ledgerNow.ID).
SetOutQty(newOutQty).
SetStatus(status).
Save(ctx0()); err != nil {
SetStatus(status)
if status == "领料完结" && ledgerNow.Status != "领料完结" {
ledgerUpd.SetCompletedAt(time.Now().Unix())
}
if _, err = ledgerUpd.Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新台账失败: "+err.Error())
return
}
@@ -242,6 +247,8 @@ func deductStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
// queryOutboundHandler 出库单查询
// 客户诉求 L96-108:按 归属单号 / 名称图号 / 日期范围 / 目标工位 查询,多条件取交集;
// 修复此前"前端传了 outboundType/日期但后端忽略"的缺陷。
func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
page := atoi(r.URL.Query().Get("page"), 1)
@@ -250,6 +257,11 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
materialCode := r.URL.Query().Get("materialCode")
outboundNo := r.URL.Query().Get("outboundNo")
boxNo := r.URL.Query().Get("boxNo")
outboundType := r.URL.Query().Get("outboundType")
outboundCategory := r.URL.Query().Get("outboundCategory")
ownershipNo := r.URL.Query().Get("ownershipNo")
materialName := r.URL.Query().Get("materialName")
targetStation := r.URL.Query().Get("targetStation")
q := ctx.EntClient.OutboundOrder.Query()
if orderNo != "" {
@@ -264,6 +276,31 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if boxNo != "" {
q = q.Where(outboundorder.BoxNoContainsFold(boxNo))
}
if outboundType != "" {
q = q.Where(outboundorder.OutboundTypeEQ(outboundType))
}
if outboundCategory != "" {
q = q.Where(outboundorder.OutboundCategoryEQ(outboundCategory))
}
if ownershipNo != "" {
q = q.Where(outboundorder.OwnershipNoContainsFold(ownershipNo))
}
if materialName != "" {
q = q.Where(outboundorder.MaterialNameContainsFold(materialName))
}
if targetStation != "" {
q = q.Where(outboundorder.TargetStationEQ(targetStation))
}
if v := r.URL.Query().Get("startDate"); v != "" {
if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil {
q = q.Where(outboundorder.CreatedAtGTE(t.Unix()))
}
}
if v := r.URL.Query().Get("endDate"); v != "" {
if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil {
q = q.Where(outboundorder.CreatedAtLTE(t.Add(24 * time.Hour).Unix()))
}
}
total, err := q.Count(ctx0())
if err != nil {
@@ -300,16 +337,22 @@ func queryOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
// generalOutboundRequest 通用出库请求(不依赖 MES 工单)
// 客户诉求 问题记录 L96-108:通用出库先点选类别(退料/样品/报废/发货/其他手输),
// 并记录归属(工单号/科技项目号/其他)与目标工位。
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"`
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"`
OutboundCategory string `json:"outboundCategory"` // return(退料)/supplier_return(退货退供应商)/sample(样品)/scrap(报废)/deliver(发货)/other(其他)
OwnershipType string `json:"ownershipType"` // workorder/project/other
OwnershipNo string `json:"ownershipNo"`
TargetStation string `json:"targetStation"`
}
// generalOutboundHandler 通用出库(手动出库,不依赖工单)
@@ -332,6 +375,15 @@ func generalOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "batchNo 或 snList 至少提供一个")
return
}
// 客户诉求 L96-108:出库类别必选;选「其他」时备注必填(承载手输原因)
if req.OutboundCategory == "" {
fail(w, http.StatusBadRequest, "出库类别必选(退料/样品/报废/发货/其他)")
return
}
if req.OutboundCategory == "other" && strings.TrimSpace(req.Remark) == "" {
fail(w, http.StatusBadRequest, "出库类别为「其他」时必须填写原因")
return
}
manageMode := 1
if len(req.SnList) > 0 {
@@ -364,6 +416,10 @@ func generalOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
ob, err := tx.OutboundOrder.Create().
SetOutboundNo(outboundNo).
SetOutboundType("general").
SetNillableOutboundCategory(strPtr(req.OutboundCategory)).
SetNillableOwnershipType(strPtr(req.OwnershipType)).
SetNillableOwnershipNo(strPtr(req.OwnershipNo)).
SetNillableTargetStation(strPtr(req.TargetStation)).
SetMaterialCode(req.MaterialCode).
SetManageMode(manageMode).
SetNillableBatchNo(strPtr(req.BatchNo)).
@@ -388,6 +444,11 @@ func generalOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "批次不存在: "+req.BatchNo)
return
}
// 质量门禁(U5,客户口径):发货/样品/其他类出库禁止不合格/待退品流向下游;退货/报废/退料类豁免(本身即处置不合格品)。
if outboundBlocksUnqualified(req.OutboundCategory) && (b.QualityStatus == "不合格" || b.QualityStatus == "待退") {
fail(w, http.StatusConflict, "批次 "+req.BatchNo+" 质量状态为「"+b.QualityStatus+"」,"+outboundCategoryLabel(req.OutboundCategory)+"出库禁止发出(请走退货/报废处置)")
return
}
avail := b.Quantity - b.LockedQty
if avail < actualQty {
fail(w, http.StatusConflict, "批次库存不足: "+req.BatchNo+" 可用 "+itoa(avail))
@@ -415,6 +476,16 @@ func generalOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// SN 扣减(电气件):仅在库/锁定的 SN 可出库
if len(req.SnList) > 0 {
// 质量门禁(U5,客户口径):发货/样品/其他类出库禁止不合格/待退 SN 流出;退货/报废/退料类豁免。
if outboundBlocksUnqualified(req.OutboundCategory) {
bad, _ := tx.Inventory.Query().
Where(inventory.ManageModeEQ(2), inventory.SnCodeIn(req.SnList...), inventory.QualityStatusIn("不合格", "待退")).
Count(ctx0())
if bad > 0 {
fail(w, http.StatusConflict, "所选 SN 含不合格/待退品,"+outboundCategoryLabel(req.OutboundCategory)+"出库禁止发出(请走退货/报废处置)")
return
}
}
for _, sn := range req.SnList {
aff, err := tx.Inventory.Update().
Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(sn), inventory.StatusIn("在库", "锁定")).
@@ -540,7 +611,7 @@ func exportOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// xlsx 导出
if r.URL.Query().Get("format") == "xlsx" {
headers := []string{"出库单号", "类型", "工单号", "物料编码", "物料名称", "数量", "装箱号", "合同号", "SN清单", "创建时间"}
headers := []string{"出库单号", "类型", "类别", "工单号", "归属单号", "图号", "物料名称", "数量", "目标工位", "装箱号", "合同号", "SN清单", "创建时间"}
matrix := make([][]any, 0, len(rows))
for _, row := range rows {
ob := row.OutboundOrder
@@ -549,8 +620,9 @@ func exportOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
tn = "备料出库"
}
matrix = append(matrix, []any{
ob.OutboundNo, tn, ob.OrderNo, ob.MaterialCode, ob.MaterialName,
ob.Quantity, ob.BoxNo, ob.ContractNo, row.SnCodes,
ob.OutboundNo, tn, outboundCategoryLabel(ob.OutboundCategory), ob.OrderNo,
ob.OwnershipNo, ob.MaterialCode, ob.MaterialName,
ob.Quantity, ob.TargetStation, ob.BoxNo, ob.ContractNo, row.SnCodes,
unixFmt(ob.CreatedAt),
})
}
@@ -568,3 +640,35 @@ func min(a, b int) int {
}
return b
}
// outboundCategoryLabel 出库类别显示中文(客户诉求 L96-108:退料/样品/报废/发货/其他)
func outboundCategoryLabel(v string) string {
switch v {
case "return":
return "退料"
case "sample":
return "样品"
case "scrap":
return "报废"
case "deliver":
return "发货"
case "supplier_return":
return "退货"
case "other":
return "其他"
default:
return ""
}
}
// outboundBlocksUnqualified 出库质量门禁判定(U5,客户口径):
// 发货(deliver)/样品(sample)/其他(other) 及任何未知类别 → 拦截不合格/待退品(默认拦截,避免漏洞);
// 退货(supplier_return,退回供应商)/报废(scrap)/退料(return,工位退料) → 豁免(本身即在处置不合格品)。
func outboundBlocksUnqualified(category string) bool {
switch category {
case "supplier_return", "scrap", "return":
return false
default:
return true
}
}