2026-09-19 07:50:21 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-19 17:04:58 +08:00
|
|
|
|
"fmt"
|
2026-09-19 07:50:21 +08:00
|
|
|
|
"net/http"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"bj_power_wms/ent/ordermaterialledger"
|
|
|
|
|
|
"bj_power_wms/internal/svc"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// backflushRequest 报工反冲核销请求(MES 报工完成时调用,X-API-TOKEN 内部接口)
|
|
|
|
|
|
//
|
|
|
|
|
|
// 口径(2026-09-19 定稿 · 方案 A「出库扣减,反冲只核销」):
|
|
|
|
|
|
// 一次物理位移 = 一次库存账变。备料出库时库存已经扣过,报工消耗**不再动库存、不再生成出库单**,
|
|
|
|
|
|
// 只把台账「已消耗(consumed_qty)」累加,供 已发出 / 已接料 / 已消耗 三方对账。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 核销上限 = net_out_qty − consumed_qty,其中 net_out_qty = out_qty − returned_qty。
|
|
|
|
|
|
// 上限的意义:没走出库的料(工位自取辅料、现场替换)不会被核销成负数,
|
|
|
|
|
|
// 避免出现「库存从未扣过、台账却记了消耗」的漏扣假象。
|
2026-09-19 07:50:21 +08:00
|
|
|
|
type backflushRequest struct {
|
2026-09-19 17:04:58 +08:00
|
|
|
|
OrderNo string `json:"orderNo"` // 关联工单号(空=不做核销)
|
|
|
|
|
|
StationNo int `json:"stationNo"` // 当前工位号(记录用)
|
2026-09-19 07:50:21 +08:00
|
|
|
|
ProcessCode int `json:"processCode"` // 当前工序号(记录用)
|
2026-09-19 17:04:58 +08:00
|
|
|
|
Operator string `json:"operator"`
|
|
|
|
|
|
Items []backflushItem `json:"items"` // 本次消耗的物料明细
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type backflushItem struct {
|
|
|
|
|
|
MaterialCode string `json:"materialCode"`
|
|
|
|
|
|
Qty int `json:"qty"` // 该工序单件消耗(已含损耗)* 完成件数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// backflushInternalHandler 报工反冲核销:只累加台账 consumed_qty。
|
|
|
|
|
|
// 不扣库存、不写出库单、不写 out_qty。库存不足/无台账不阻塞产线,只记 shortfall 供对账。
|
2026-09-19 07:50:21 +08:00
|
|
|
|
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 {
|
2026-09-19 17:04:58 +08:00
|
|
|
|
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": "未挂工单,不做核销"})
|
2026-09-19 07:50:21 +08:00
|
|
|
|
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
|
2026-09-19 17:04:58 +08:00
|
|
|
|
notes := make([]string, 0, 4)
|
2026-09-19 07:50:21 +08:00
|
|
|
|
for _, it := range req.Items {
|
|
|
|
|
|
if it.MaterialCode == "" || it.Qty <= 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
ledger, lerr := tx.OrderMaterialLedger.Query().
|
|
|
|
|
|
Where(
|
|
|
|
|
|
ordermaterialledger.OrderNoEQ(req.OrderNo),
|
|
|
|
|
|
ordermaterialledger.MaterialCodeEQ(it.MaterialCode),
|
|
|
|
|
|
).Only(ctx0())
|
|
|
|
|
|
if lerr != nil || ledger == nil {
|
2026-09-19 07:50:21 +08:00
|
|
|
|
shortfall = true
|
2026-09-19 17:04:58 +08:00
|
|
|
|
notes = append(notes, it.MaterialCode+" 无台账")
|
|
|
|
|
|
continue
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
// 可核销量 = 净领用 − 已消耗(负数归零:退料后可能出现「已消耗 > 净领用」)
|
|
|
|
|
|
capa := (ledger.OutQty - ledger.ReturnedQty) - ledger.ConsumedQty
|
|
|
|
|
|
if capa < 0 {
|
|
|
|
|
|
capa = 0
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
2026-09-19 17:04:58 +08:00
|
|
|
|
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
|
2026-09-19 07:50:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
|
|
fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
committed = true
|
|
|
|
|
|
|
2026-09-19 17:04:58 +08:00
|
|
|
|
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)
|
2026-09-19 07:50:21 +08:00
|
|
|
|
ok(w, map[string]any{
|
2026-09-19 17:04:58 +08:00
|
|
|
|
"consumed": totalConsumed,
|
|
|
|
|
|
"shortfall": shortfall,
|
|
|
|
|
|
"notes": notes,
|
2026-09-19 07:50:21 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|