Files
bj_power/bj_power_wms/internal/handler/backflush.go
T

214 lines
7.1 KiB
Go
Raw Normal View History

package handler
import (
"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 扣减与记账。
type backflushRequest struct {
OrderNo string `json:"orderNo"` // 关联工单号(可空=不挂台账)
StationNo int `json:"stationNo"` // 当前工位号(记录用)
ProcessCode int `json:"processCode"` // 当前工序号(记录用)
Operator string `json:"operator"`
Items []backflushItem `json:"items"` // 本次消耗的物料明细
}
type backflushItem struct {
MaterialCode string `json:"materialCode"`
Qty int `json:"qty"` // 该工序单件消耗(已含损耗)* 完成件数
}
// backflushInternalHandler 反冲扣账(库存 FIFO 扣减 + 生成 backflush 类型出库单 + 台账累计)
// 事务保证:出库单 + 库存扣减 + 出库明细 + 工单台账 原子提交。
// 任意物料库存不足(FIFO 无法足额)时,扣足可用部分、记短欠日志、不阻塞产线(反冲降级同成品回流)。
func backflushInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req backflushRequest
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
return
}
if len(req.Items) == 0 {
ok(w, map[string]any{"outboundNo": "", "consumed": 0, "shortfall": false, "note": "无反冲物料"})
return
}
outboundNo := "BF" + 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()
}
}()
// 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
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 {
shortfall = true
logx.Errorf("backflush shortfall: order=%s material=%s need=%d got=%d", req.OrderNo, it.MaterialCode, it.Qty, consumed)
}
// 台账累计(仅挂工单时)
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
}
}
}
}
// 3. 回写出库单实际数量
if _, err = tx.OutboundOrder.UpdateOneID(ob.ID).SetQuantity(totalConsumed).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(), "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)
ok(w, map[string]any{
"outboundNo": outboundNo,
"consumed": totalConsumed,
"shortfall": shortfall,
})
}
}
// 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
}