feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统 - 为BomItem实体添加relatedStandard字段及相关CRUD方法 - 为InspectionRecord实体添加reportNo、materialCode、materialName等字段 - 更新ent schema确保新字段的验证和默认值设置 - 添加必要的数据库迁移和字段映射逻辑
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user