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

127 lines
4.5 KiB
Go
Raw Normal View History

package handler
import (
"fmt"
"net/http"
"strconv"
"time"
"bj_power_wms/ent/ordermaterialledger"
"bj_power_wms/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
// 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"` // 当前工位号(记录用)
ProcessCode int `json:"processCode"` // 当前工序号(记录用)
Operator string `json:"operator"`
Items []backflushItem `json:"items"` // 本次消耗的物料明细
}
type backflushItem struct {
MaterialCode string `json:"materialCode"`
Qty int `json:"qty"` // 该工序单件消耗(已含损耗)* 完成件数
}
// backflushInternalHandler 报工反冲核销:只累加台账 consumed_qty。
// 不扣库存、不写出库单、不写 out_qty。库存不足/无台账不阻塞产线,只记 shortfall 供对账。
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{"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
}
tx, err := ctx.EntClient.Tx(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
return
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
totalConsumed := 0
shortfall := false
notes := make([]string, 0, 4)
for _, it := range req.Items {
if it.MaterialCode == "" || it.Qty <= 0 {
continue
}
ledger, lerr := tx.OrderMaterialLedger.Query().
Where(
ordermaterialledger.OrderNoEQ(req.OrderNo),
ordermaterialledger.MaterialCodeEQ(it.MaterialCode),
).Only(ctx0())
if lerr != nil || ledger == nil {
shortfall = true
notes = append(notes, it.MaterialCode+" 无台账")
continue
}
// 可核销量 = 净领用 − 已消耗(负数归零:退料后可能出现「已消耗 > 净领用」)
capa := (ledger.OutQty - ledger.ReturnedQty) - ledger.ConsumedQty
if capa < 0 {
capa = 0
}
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 {
fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error())
return
}
committed = true
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{
"consumed": totalConsumed,
"shortfall": shortfall,
"notes": notes,
})
}
}