1. 新增预警表添加工单号字段,支持按工单号检索预警 2. 添加工单过滤在制品/绩效报表/预警中心查询 3. 补全WMS台账字段口径与权限配置 4. 修复备料单与数量不符处理流程 5. 新增配送进度/库位联动/自动出库功能 6. 修复AGV配送状态更新逻辑 7. 优化退料与库存管理细节
180 lines
6.1 KiB
Go
180 lines
6.1 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
|
||
"bj_power_mes/ent/bomitem"
|
||
)
|
||
|
||
// ============================================================
|
||
// BOM 展开统一核心(R1 工程债,2026-09-19)
|
||
// ============================================================
|
||
//
|
||
// 背景:备料算料(expandBomToLeaf)与追溯物料组成(expandBindBom)原先各自实现了一套
|
||
// 递归展开,两套的"是否继续下钻"判定不同——
|
||
// - 备料:子项在 WMS 的品类为成品(3) 才继续展开;
|
||
// - 追溯:本地存在该编码的下级物料清单才继续展开。
|
||
// 结果是同一产品在"备料料单"和"追溯物料组成"里可能呈现不同结构,追溯断链。
|
||
//
|
||
// 此处抽出唯一递归骨架,两处共用同一份实现:深度上限、防环、用量倍率相乘、日志口径
|
||
// 完全一致;下钻判定与 BOM 名称策略由调用方以 resolver/参数注入,保证业务口径不变。
|
||
|
||
// bomNode BOM 展开的中间态树节点(备料拍平为叶子、追溯直接展示层级)。
|
||
type bomNode struct {
|
||
MaterialCode string
|
||
MaterialName string
|
||
Spec string
|
||
Unit string
|
||
ManageMode string
|
||
UnitQty float64 // 已按路径倍率折算的单台用量
|
||
LossRate float64
|
||
ProcessCode int
|
||
Level int // 1=顶层物料,2+=下级
|
||
Children []bomNode
|
||
}
|
||
|
||
// bomMaxDepth 展开深度上限(防配置错误导致的无界递归)。
|
||
const bomMaxDepth = 10
|
||
|
||
// bomCompositeResolver 批量判定本层哪些子项属于"组合件"(需继续下钻展开其自身清单)。
|
||
// 每层只调用一次,避免逐条查 WMS/库造成 N+1。
|
||
type bomCompositeResolver func(ctx context.Context, productCode string, codes []string) map[string]bool
|
||
|
||
// expandBomTree 统一递归展开核心。
|
||
// - productCode: 当前层产品/组合件编码;
|
||
// - bomName: 物料清单名称("" 由 ListBom 决定不过滤,具体策略由调用方传入);
|
||
// - mult: 父级累计倍率;
|
||
// - path: 当前展开路径(防环,同路径重复编码视为配置错误并记日志)。
|
||
func (s *Service) expandBomTree(ctx context.Context, productCode, bomName string, mult float64, depth int,
|
||
path map[string]bool, resolve bomCompositeResolver) []bomNode {
|
||
if depth > bomMaxDepth || productCode == "" {
|
||
return nil
|
||
}
|
||
bom, err := s.ListBom(ctx, productCode, bomName)
|
||
if err != nil || len(bom) == 0 {
|
||
return nil
|
||
}
|
||
codes := make([]string, 0, len(bom))
|
||
for _, b := range bom {
|
||
if b.MaterialCode != "" {
|
||
codes = append(codes, b.MaterialCode)
|
||
}
|
||
}
|
||
var composite map[string]bool
|
||
if resolve != nil {
|
||
composite = resolve(ctx, productCode, codes)
|
||
}
|
||
|
||
out := make([]bomNode, 0, len(bom))
|
||
for _, b := range bom {
|
||
if b.MaterialCode == "" {
|
||
continue
|
||
}
|
||
if path[b.MaterialCode] {
|
||
s.ctx.EventLog.Write(ctx, "bom.expand.cycle", "", "", "product_bom", productCode,
|
||
"产品物料清单存在循环引用,已跳过",
|
||
map[string]any{"materialCode": b.MaterialCode, "depth": depth})
|
||
continue
|
||
}
|
||
unitQty := b.UnitQty * mult
|
||
node := bomNode{
|
||
MaterialCode: b.MaterialCode,
|
||
MaterialName: b.MaterialName,
|
||
Spec: b.Spec,
|
||
Unit: b.Unit,
|
||
ManageMode: b.ManageMode,
|
||
UnitQty: unitQty,
|
||
LossRate: b.LossRate,
|
||
ProcessCode: b.ProcessCode,
|
||
Level: depth,
|
||
}
|
||
if composite[b.MaterialCode] {
|
||
path[b.MaterialCode] = true
|
||
node.Children = s.expandBomTree(ctx, b.MaterialCode, bomName, unitQty, depth+1, path, resolve)
|
||
delete(path, b.MaterialCode)
|
||
// 组合件没有自身清单(或展开为空):按叶子处理,避免漏料
|
||
if len(node.Children) == 0 {
|
||
node.Children = nil
|
||
}
|
||
}
|
||
out = append(out, node)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// bomTreeToLeaf 把展开树拍平为叶子清单(备料算料用):组合件递归下沉,叶子原样输出。
|
||
func bomTreeToLeaf(nodes []bomNode) []bomLeaf {
|
||
out := make([]bomLeaf, 0, len(nodes))
|
||
for _, n := range nodes {
|
||
if len(n.Children) > 0 {
|
||
out = append(out, bomTreeToLeaf(n.Children)...)
|
||
continue
|
||
}
|
||
out = append(out, bomLeaf{
|
||
materialCode: n.MaterialCode,
|
||
materialName: n.MaterialName,
|
||
unit: n.Unit,
|
||
manageMode: n.ManageMode,
|
||
unitQty: n.UnitQty,
|
||
lossRate: n.LossRate,
|
||
processCode: n.ProcessCode,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// bomTreeToBindNodes 把展开树转为追溯展示节点(保留层级与用量)。
|
||
func bomTreeToBindNodes(nodes []bomNode) []BindNode {
|
||
out := make([]BindNode, 0, len(nodes))
|
||
for _, n := range nodes {
|
||
node := BindNode{
|
||
MaterialCode: n.MaterialCode,
|
||
MaterialName: n.MaterialName,
|
||
Spec: n.Spec,
|
||
Unit: n.Unit,
|
||
ManageMode: n.ManageMode,
|
||
Qty: n.UnitQty,
|
||
Level: n.Level,
|
||
}
|
||
if len(n.Children) > 0 {
|
||
node.Children = bomTreeToBindNodes(n.Children)
|
||
}
|
||
out = append(out, node)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// wmsFinishedResolver 备料侧下钻判定:子项在 WMS 的品类为成品(3) 时继续展开。
|
||
// 客户口径:成品可作为零部件被再次加工,备料必须展开到原材料,否则产线缺料。
|
||
// WMS 不可达时返回空集(全部按叶子处理),不阻塞备料生成,并记日志。
|
||
func (s *Service) wmsFinishedResolver(ctx context.Context, productCode string, codes []string) map[string]bool {
|
||
out := map[string]bool{}
|
||
if s.ctx.Wms == nil || len(codes) == 0 {
|
||
return out
|
||
}
|
||
types, err := s.ctx.Wms.MaterialTypes(ctx, codes)
|
||
if err != nil {
|
||
s.ctx.EventLog.Write(ctx, "bom.expand.wms_skip", "", "", "product_bom", productCode,
|
||
"WMS 物料品类查询不可达,本次按叶子物料展开", map[string]any{"error": err.Error()})
|
||
return out
|
||
}
|
||
for _, c := range codes {
|
||
if types[c] == 3 {
|
||
out[c] = true
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// localBomExistsResolver 追溯侧下钻判定:本地存在该编码的下级物料清单时继续展开。
|
||
// 不依赖 WMS(追溯页在 WMS 不可达时也必须能展示完整物料组成)。
|
||
func (s *Service) localBomExistsResolver(ctx context.Context, _ string, codes []string) map[string]bool {
|
||
out := map[string]bool{}
|
||
for _, c := range codes {
|
||
if has, _ := s.ctx.EntClient.BomItem.Query().Where(bomitem.ProductCode(c)).Exist(ctx); has {
|
||
out[c] = true
|
||
}
|
||
}
|
||
return out
|
||
}
|