213 lines
7.0 KiB
Go
213 lines
7.0 KiB
Go
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.QualityStatusNotIn("不合格", "待退"),
|
|||
|
|
).
|
|||
|
|
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
|
|||
|
|
}
|