1. 移除工单、工件、日排产中的冗余工位组合/接驳台字段 2. 新增工位接驳台编码字段与唯一约束,关联WMS接驳台主数据 3. 重构日排产为工位产量分配模式,替代原接驳台列表 4. 新增备料单工位级字段与分批出库支持 5. 移除定时同步可生产数量,改为WMS实时拉取接口 6. 过滤操作日志,排除登录/退出相关日志 7. 调整仪表盘工序展示逻辑为今日派工路线 8. 新增接驳台维护菜单与基础数据
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
|
||
}
|