feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警 2. 添加工单过滤在制品/绩效报表/预警中心查询 3. 补全WMS台账字段口径与权限配置 4. 修复备料单与数量不符处理流程 5. 新增配送进度/库位联动/自动出库功能 6. 修复AGV配送状态更新逻辑 7. 优化退料与库存管理细节
This commit is contained in:
@@ -1,28 +1,32 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
"bj_power_wms/ent/inventory"
|
||||
"bj_power_wms/ent/ordermaterialledger"
|
||||
"bj_power_wms/internal/svc"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// backflushRequest 反冲扣账请求(MES 报工完成时调用,X-API-TOKEN 内部接口)
|
||||
// 物料在该道工序被消耗,不依赖事先发料:报工完成即反冲扣库存。
|
||||
// items 由 MES 按「工单产品 BOM + 当前工序 processCode」算好(含损耗),WMS 只负责 FIFO 扣减与记账。
|
||||
// backflushRequest 报工反冲核销请求(MES 报工完成时调用,X-API-TOKEN 内部接口)
|
||||
//
|
||||
// 口径(2026-09-19 定稿 · 方案 A「出库扣减,反冲只核销」):
|
||||
// 一次物理位移 = 一次库存账变。备料出库时库存已经扣过,报工消耗**不再动库存、不再生成出库单**,
|
||||
// 只把台账「已消耗(consumed_qty)」累加,供 已发出 / 已接料 / 已消耗 三方对账。
|
||||
//
|
||||
// 核销上限 = net_out_qty − consumed_qty,其中 net_out_qty = out_qty − returned_qty。
|
||||
// 上限的意义:没走出库的料(工位自取辅料、现场替换)不会被核销成负数,
|
||||
// 避免出现「库存从未扣过、台账却记了消耗」的漏扣假象。
|
||||
type backflushRequest struct {
|
||||
OrderNo string `json:"orderNo"` // 关联工单号(可空=不挂台账)
|
||||
StationNo int `json:"stationNo"` // 当前工位号(记录用)
|
||||
OrderNo string `json:"orderNo"` // 关联工单号(空=不做核销)
|
||||
StationNo int `json:"stationNo"` // 当前工位号(记录用)
|
||||
ProcessCode int `json:"processCode"` // 当前工序号(记录用)
|
||||
Operator string `json:"operator"`
|
||||
Items []backflushItem `json:"items"` // 本次消耗的物料明细
|
||||
Operator string `json:"operator"`
|
||||
Items []backflushItem `json:"items"` // 本次消耗的物料明细
|
||||
}
|
||||
|
||||
type backflushItem struct {
|
||||
@@ -30,9 +34,8 @@ type backflushItem struct {
|
||||
Qty int `json:"qty"` // 该工序单件消耗(已含损耗)* 完成件数
|
||||
}
|
||||
|
||||
// backflushInternalHandler 反冲扣账(库存 FIFO 扣减 + 生成 backflush 类型出库单 + 台账累计)
|
||||
// 事务保证:出库单 + 库存扣减 + 出库明细 + 工单台账 原子提交。
|
||||
// 任意物料库存不足(FIFO 无法足额)时,扣足可用部分、记短欠日志、不阻塞产线(反冲降级同成品回流)。
|
||||
// backflushInternalHandler 报工反冲核销:只累加台账 consumed_qty。
|
||||
// 不扣库存、不写出库单、不写 out_qty。库存不足/无台账不阻塞产线,只记 shortfall 供对账。
|
||||
func backflushInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req backflushRequest
|
||||
@@ -41,11 +44,15 @@ func backflushInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
ok(w, map[string]any{"outboundNo": "", "consumed": 0, "shortfall": false, "note": "无反冲物料"})
|
||||
ok(w, map[string]any{"consumed": 0, "shortfall": false, "note": "无反冲物料"})
|
||||
return
|
||||
}
|
||||
if req.OrderNo == "" {
|
||||
// 不挂工单的消耗没有台账可承载,不阻塞产线
|
||||
logx.Infof("backflush skipped: no orderNo, station=%d", req.StationNo)
|
||||
ok(w, map[string]any{"consumed": 0, "shortfall": false, "note": "未挂工单,不做核销"})
|
||||
return
|
||||
}
|
||||
|
||||
outboundNo := "BF" + uuid.NewString()[:12]
|
||||
|
||||
tx, err := ctx.EntClient.Tx(ctx0())
|
||||
if err != nil {
|
||||
@@ -59,67 +66,45 @@ func backflushInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. 创建反冲出库单(type=backflush,数量后置更新)
|
||||
ob, err := tx.OutboundOrder.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetOutboundType("backflush").
|
||||
SetNillableOrderNo(strPtr(req.OrderNo)).
|
||||
SetMaterialCode(""). // 反冲单为整份 BOM,主表物料留空,明细承载
|
||||
SetManageMode(1).
|
||||
SetQuantity(0).
|
||||
SetNillableOperator(strPtr(req.Operator)).
|
||||
SetNillableTargetStation(strPtr(strconv.Itoa(req.StationNo))).
|
||||
SetRemark("反冲扣账(工序" + strconv.Itoa(req.ProcessCode) + ")").
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建反冲单失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. FIFO 逐物料扣减 + 台账累计
|
||||
totalConsumed := 0
|
||||
shortfall := false
|
||||
notes := make([]string, 0, 4)
|
||||
for _, it := range req.Items {
|
||||
if it.MaterialCode == "" || it.Qty <= 0 {
|
||||
continue
|
||||
}
|
||||
consumed, sf := fifoConsume(tx, it.MaterialCode, it.Qty, outboundNo, req.OrderNo)
|
||||
totalConsumed += consumed
|
||||
if sf {
|
||||
ledger, lerr := tx.OrderMaterialLedger.Query().
|
||||
Where(
|
||||
ordermaterialledger.OrderNoEQ(req.OrderNo),
|
||||
ordermaterialledger.MaterialCodeEQ(it.MaterialCode),
|
||||
).Only(ctx0())
|
||||
if lerr != nil || ledger == nil {
|
||||
shortfall = true
|
||||
logx.Errorf("backflush shortfall: order=%s material=%s need=%d got=%d", req.OrderNo, it.MaterialCode, it.Qty, consumed)
|
||||
notes = append(notes, it.MaterialCode+" 无台账")
|
||||
continue
|
||||
}
|
||||
// 台账累计(仅挂工单时)
|
||||
if req.OrderNo != "" {
|
||||
ledger, lerr := tx.OrderMaterialLedger.Query().
|
||||
Where(
|
||||
ordermaterialledger.OrderNoEQ(req.OrderNo),
|
||||
ordermaterialledger.MaterialCodeEQ(it.MaterialCode),
|
||||
).Only(ctx0())
|
||||
if lerr == nil && ledger != nil {
|
||||
newOut := ledger.OutQty + consumed
|
||||
status := ledger.Status
|
||||
if newOut >= ledger.TotalQty {
|
||||
status = "领料完结"
|
||||
}
|
||||
upd := tx.OrderMaterialLedger.UpdateOneID(ledger.ID).
|
||||
SetOutQty(newOut).
|
||||
SetStatus(status)
|
||||
if status == "领料完结" && ledger.Status != "领料完结" {
|
||||
upd.SetCompletedAt(time.Now().Unix())
|
||||
}
|
||||
if _, lerr = upd.Save(ctx0()); lerr != nil {
|
||||
fail(w, http.StatusInternalServerError, "更新台账失败: "+lerr.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
// 可核销量 = 净领用 − 已消耗(负数归零:退料后可能出现「已消耗 > 净领用」)
|
||||
capa := (ledger.OutQty - ledger.ReturnedQty) - ledger.ConsumedQty
|
||||
if capa < 0 {
|
||||
capa = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 回写出库单实际数量
|
||||
if _, err = tx.OutboundOrder.UpdateOneID(ob.ID).SetQuantity(totalConsumed).Save(ctx0()); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "更新反冲单失败: "+err.Error())
|
||||
return
|
||||
take := it.Qty
|
||||
if take > capa {
|
||||
take = capa
|
||||
shortfall = true
|
||||
notes = append(notes, fmt.Sprintf("%s 超出可核销量(需%d/可%d)", it.MaterialCode, it.Qty, capa))
|
||||
}
|
||||
if take <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, uerr := tx.OrderMaterialLedger.UpdateOneID(ledger.ID).
|
||||
AddConsumedQty(take).
|
||||
SetUpdatedAt(time.Now().Unix()).
|
||||
Save(ctx0()); uerr != nil {
|
||||
fail(w, http.StatusInternalServerError, "更新台账失败: "+uerr.Error())
|
||||
return
|
||||
}
|
||||
totalConsumed += take
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
@@ -128,86 +113,14 @@ func backflushInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
committed = true
|
||||
|
||||
ctx.EventLog.Write(ctx0(), "material.backflush", req.Operator, "backflush", outboundNo,
|
||||
"反冲扣账 工单 "+req.OrderNo+" 工序"+strconv.Itoa(req.ProcessCode)+" 共消耗 ×"+strconv.Itoa(totalConsumed),
|
||||
map[string]any{"orderNo": req.OrderNo, "stationNo": req.StationNo, "consumed": totalConsumed, "shortfall": shortfall})
|
||||
logx.Infof("backflush: order=%s station=%d consumed=%d shortfall=%v", req.OrderNo, req.StationNo, totalConsumed, shortfall)
|
||||
ctx.EventLog.Write(ctx0(), "material.backflush", req.Operator, "order_material_ledger", req.OrderNo,
|
||||
"报工消耗核销 工单 "+req.OrderNo+" 工序"+strconv.Itoa(req.ProcessCode)+" 共核销 ×"+strconv.Itoa(totalConsumed),
|
||||
map[string]any{"orderNo": req.OrderNo, "stationNo": req.StationNo, "consumed": totalConsumed, "shortfall": shortfall, "notes": notes})
|
||||
logx.Infof("backflush(consume-only): order=%s station=%d consumed=%d shortfall=%v", req.OrderNo, req.StationNo, totalConsumed, shortfall)
|
||||
ok(w, map[string]any{
|
||||
"outboundNo": outboundNo,
|
||||
"consumed": totalConsumed,
|
||||
"shortfall": shortfall,
|
||||
"consumed": totalConsumed,
|
||||
"shortfall": shortfall,
|
||||
"notes": notes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fifoConsume 按 created_at 升序(先进先出)扣减某物料库存,跨批次/单件 SN 统一处理。
|
||||
// 返回实际扣减数;库存不足足额时 shortfall=true(已扣可用部分),不报错。
|
||||
func fifoConsume(tx *ent.Tx, materialCode string, qty int, outboundNo, orderNo string) (int, bool) {
|
||||
rows, err := tx.Inventory.Query().
|
||||
Where(
|
||||
inventory.MaterialCodeEQ(materialCode),
|
||||
inventory.StatusEQ("在库"),
|
||||
// 可用口径(客户口径):仅「合格」可被消耗(未检/不合格/待退 不计可用)
|
||||
inventory.QualityStatusEQ(QStatusPass),
|
||||
).
|
||||
Order(ent.Asc("created_at")).
|
||||
All(ctx0())
|
||||
if err != nil {
|
||||
return 0, true
|
||||
}
|
||||
remaining := qty
|
||||
consumed := 0
|
||||
for _, row := range rows {
|
||||
if remaining <= 0 {
|
||||
break
|
||||
}
|
||||
avail := row.Quantity - row.LockedQty
|
||||
if avail <= 0 {
|
||||
continue
|
||||
}
|
||||
take := avail
|
||||
if take > remaining {
|
||||
take = remaining
|
||||
}
|
||||
if row.ManageMode == 2 {
|
||||
// SN 单件:整件消耗并置出库
|
||||
aff, e := tx.Inventory.Update().
|
||||
Where(inventory.ID(row.ID), inventory.StatusEQ("在库")).
|
||||
SetStatus("出库").Save(ctx0())
|
||||
if e != nil || aff == 0 {
|
||||
continue
|
||||
}
|
||||
if _, e = tx.OutboundDetail.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetSnCode(row.SnCode).
|
||||
SetMaterialCode(materialCode).
|
||||
SetQuantity(1).
|
||||
SetNillableOrderNo(strPtr(orderNo)).Save(ctx0()); e != nil {
|
||||
return consumed, true
|
||||
}
|
||||
consumed += 1
|
||||
remaining -= 1
|
||||
} else {
|
||||
// 批次:条件原子扣减(quantity>=take 才减,避免超扣/并发负库存)
|
||||
aff, e := tx.Inventory.Update().
|
||||
Where(inventory.ID(row.ID), inventory.QuantityGTE(take)).
|
||||
AddQuantity(-take).
|
||||
AddLockedQty(-min(take, row.LockedQty)).
|
||||
Save(ctx0())
|
||||
if e != nil || aff == 0 {
|
||||
continue
|
||||
}
|
||||
if _, e = tx.OutboundDetail.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetBatchNo(row.BatchNo).
|
||||
SetMaterialCode(materialCode).
|
||||
SetQuantity(take).
|
||||
SetNillableOrderNo(strPtr(orderNo)).Save(ctx0()); e != nil {
|
||||
return consumed, true
|
||||
}
|
||||
consumed += take
|
||||
remaining -= take
|
||||
}
|
||||
}
|
||||
return consumed, remaining > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user