- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统 - 为BomItem实体添加relatedStandard字段及相关CRUD方法 - 为InspectionRecord实体添加reportNo、materialCode、materialName等字段 - 更新ent schema确保新字段的验证和默认值设置 - 添加必要的数据库迁移和字段映射逻辑
255 lines
7.3 KiB
Go
255 lines
7.3 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"bj_power_mes/ent/dailyplan"
|
|
"bj_power_mes/ent/workorder"
|
|
"bj_power_mes/internal/wmsclient"
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
)
|
|
|
|
// producibleBomItem 可生产数量计算用的精简 BOM 行
|
|
type producibleBomItem struct {
|
|
materialCode string
|
|
materialName string
|
|
unitQty float64 // 单台用量
|
|
}
|
|
|
|
// SyncProducibleToWMS 计算「可生产数量」快照 + 「未来5天物料需求」并推送到 WMS。
|
|
//
|
|
// 业务口径(问题记录 L470-474 + L256):
|
|
// - 可生产套数 = min(各物料可用量 ÷ 单台用量)。BOM 单台用量在 MES、库存可用量在 WMS,
|
|
// 故由 MES 拉取 WMS 可用量(/api/internal/stock/check)后本地计算,再把快照推回 WMS 只读展示。
|
|
// - 未来5天需求量 = 近5天(含今日)日排产台数 × 单台用量,按物料汇总,供 WMS 备料四态「预警」判定。
|
|
//
|
|
// 两系统独立运行约束:仅 MES→WMS 单向调用;未配置 WMS 或不可达时记录日志并跳过,绝不阻塞 MES。
|
|
func (s *Service) SyncProducibleToWMS(ctx context.Context) error {
|
|
if s.ctx.Wms == nil || s.ctx.EntClient == nil {
|
|
return nil
|
|
}
|
|
all, err := s.ctx.EntClient.BomItem.Query().All(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(all) == 0 {
|
|
return nil // 无 BOM 不推送
|
|
}
|
|
|
|
// 1) 按 产品编码 → BOM名称 → 明细行 分组
|
|
byProd := map[string]map[string][]producibleBomItem{}
|
|
for _, b := range all {
|
|
if b.ProductCode == "" || b.MaterialCode == "" {
|
|
continue
|
|
}
|
|
bn := b.BomName
|
|
if bn == "" {
|
|
bn = DefaultBOMName
|
|
}
|
|
if byProd[b.ProductCode] == nil {
|
|
byProd[b.ProductCode] = map[string][]producibleBomItem{}
|
|
}
|
|
byProd[b.ProductCode][bn] = append(byProd[b.ProductCode][bn], producibleBomItem{
|
|
materialCode: b.MaterialCode, materialName: b.MaterialName, unitQty: b.UnitQty,
|
|
})
|
|
}
|
|
|
|
// 2) 每个产品选定一份 BOM:优先「默认」,否则按名排序取第一份
|
|
chosen := map[string][]producibleBomItem{}
|
|
materialSet := map[string]bool{}
|
|
for pc, boms := range byProd {
|
|
var bn string
|
|
if _, has := boms[DefaultBOMName]; has {
|
|
bn = DefaultBOMName
|
|
} else {
|
|
names := make([]string, 0, len(boms))
|
|
for n := range boms {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
bn = names[0]
|
|
}
|
|
chosen[pc] = boms[bn]
|
|
for _, it := range boms[bn] {
|
|
materialSet[it.materialCode] = true
|
|
}
|
|
}
|
|
|
|
// 3) 拉取 WMS 各物料可用量
|
|
codes := make([]string, 0, len(materialSet))
|
|
for c := range materialSet {
|
|
codes = append(codes, c)
|
|
}
|
|
sort.Strings(codes)
|
|
avail, err := s.ctx.Wms.StockAvailable(ctx, codes)
|
|
if err != nil {
|
|
logx.Errorf("[producible] 拉取 WMS 可用量失败,跳过本次同步: %v", err)
|
|
return err
|
|
}
|
|
|
|
// 4) 产品名称映射(product_type 优先,work_order 兜底)
|
|
nameMap := map[string]string{}
|
|
if pts, e := s.ctx.EntClient.ProductType.Query().All(ctx); e == nil {
|
|
for _, p := range pts {
|
|
nameMap[p.Code] = p.Name
|
|
}
|
|
}
|
|
if wos, e := s.ctx.EntClient.WorkOrder.Query().All(ctx); e == nil {
|
|
for _, wo := range wos {
|
|
if wo.ProductCode != "" && wo.ProductName != "" {
|
|
if _, has := nameMap[wo.ProductCode]; !has {
|
|
nameMap[wo.ProductCode] = wo.ProductName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5) 逐产品计算可生产套数 = min(floor(可用量 ÷ 单台用量)),短板物料取达最小值的前3
|
|
now := time.Now().Unix()
|
|
prodCodes := make([]string, 0, len(chosen))
|
|
for pc := range chosen {
|
|
prodCodes = append(prodCodes, pc)
|
|
}
|
|
sort.Strings(prodCodes)
|
|
items := make([]wmsclient.ProducibleItem, 0, len(prodCodes))
|
|
for _, pc := range prodCodes {
|
|
bom := chosen[pc]
|
|
minSets := -1
|
|
type shortRow struct {
|
|
text string
|
|
set int
|
|
}
|
|
shorts := make([]shortRow, 0, len(bom))
|
|
for _, it := range bom {
|
|
if it.unitQty <= 0 {
|
|
continue
|
|
}
|
|
a := avail[it.materialCode]
|
|
sets := int(math.Floor(float64(a) / it.unitQty))
|
|
if sets < 0 {
|
|
sets = 0
|
|
}
|
|
if minSets < 0 || sets < minSets {
|
|
minSets = sets
|
|
}
|
|
name := it.materialName
|
|
if name == "" {
|
|
name = it.materialCode
|
|
}
|
|
shorts = append(shorts, shortRow{
|
|
text: fmt.Sprintf("%s(%s) 可用%d÷单台%s=%d套", name, it.materialCode, a, trimNum(it.unitQty), sets),
|
|
set: sets,
|
|
})
|
|
}
|
|
if minSets < 0 {
|
|
minSets = 0 // BOM 全无有效单台用量
|
|
}
|
|
sort.SliceStable(shorts, func(i, j int) bool { return shorts[i].set < shorts[j].set })
|
|
pick := make([]string, 0, 3)
|
|
for _, sr := range shorts {
|
|
if sr.set == minSets && len(pick) < 3 {
|
|
pick = append(pick, sr.text)
|
|
}
|
|
}
|
|
items = append(items, wmsclient.ProducibleItem{
|
|
ProductCode: pc,
|
|
ProductName: nameMap[pc],
|
|
ProducibleQty: minSets,
|
|
ShortText: strings.Join(pick, "; "),
|
|
ComputedAt: now,
|
|
})
|
|
}
|
|
|
|
// 6) 未来5天物料需求(备料四态「预警」数据源)
|
|
demands := s.computeFuture5Demand(ctx, chosen)
|
|
|
|
if err := s.ctx.Wms.SyncProducible(ctx, items, demands); err != nil {
|
|
logx.Errorf("[producible] 推送 WMS 失败: %v", err)
|
|
return err
|
|
}
|
|
logx.Infof("[producible] 已同步可生产数量 %d 项、未来5天需求 %d 项到 WMS", len(items), len(demands))
|
|
return nil
|
|
}
|
|
|
|
// computeFuture5Demand 近5天(含今日)日排产台数 × 单台用量,按物料汇总为未来5天需求量。
|
|
// 数据链路:daily_plan(工单号→台数) → work_order(工单号→产品编码) → BOM(产品编码→物料单台用量)。
|
|
func (s *Service) computeFuture5Demand(ctx context.Context, chosen map[string][]producibleBomItem) []wmsclient.ProducibleDemand {
|
|
today := time.Now()
|
|
start := today.Format("2006-01-02")
|
|
end := today.AddDate(0, 0, 4).Format("2006-01-02")
|
|
plans, err := s.ctx.EntClient.DailyPlan.Query().
|
|
Where(dailyplan.PlanDateGTE(start), dailyplan.PlanDateLTE(end)).All(ctx)
|
|
if err != nil || len(plans) == 0 {
|
|
return nil
|
|
}
|
|
// 工单号 → 未来5天台数(排除已取消)
|
|
orderByNo := map[string]int{}
|
|
for _, p := range plans {
|
|
if p.Status == "CANCELLED" {
|
|
continue
|
|
}
|
|
orderByNo[p.OrderNo] += p.PlanQty
|
|
}
|
|
if len(orderByNo) == 0 {
|
|
return nil
|
|
}
|
|
nos := make([]string, 0, len(orderByNo))
|
|
for no := range orderByNo {
|
|
nos = append(nos, no)
|
|
}
|
|
wos, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNoIn(nos...)).All(ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
// 产品编码 → 未来5天台数
|
|
prodQty := map[string]int{}
|
|
for _, wo := range wos {
|
|
if wo.ProductCode == "" {
|
|
continue
|
|
}
|
|
prodQty[wo.ProductCode] += orderByNo[wo.WorkOrderNo]
|
|
}
|
|
// 物料汇总:台数 × 单台用量(向上取整)
|
|
matQty := map[string]int{}
|
|
for pc, qty := range prodQty {
|
|
bom := chosen[pc]
|
|
if qty <= 0 || len(bom) == 0 {
|
|
continue
|
|
}
|
|
for _, it := range bom {
|
|
if it.unitQty <= 0 {
|
|
continue
|
|
}
|
|
matQty[it.materialCode] += int(math.Ceil(float64(qty) * it.unitQty))
|
|
}
|
|
}
|
|
if len(matQty) == 0 {
|
|
return nil
|
|
}
|
|
mcodes := make([]string, 0, len(matQty))
|
|
for c := range matQty {
|
|
mcodes = append(mcodes, c)
|
|
}
|
|
sort.Strings(mcodes)
|
|
out := make([]wmsclient.ProducibleDemand, 0, len(mcodes))
|
|
for _, c := range mcodes {
|
|
out = append(out, wmsclient.ProducibleDemand{MaterialCode: c, Future5Qty: matQty[c]})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// trimNum 单台用量展示:整数不带小数点,非整数保留原精度
|
|
func trimNum(f float64) string {
|
|
if f == math.Trunc(f) {
|
|
return strconv.Itoa(int(f))
|
|
}
|
|
return strconv.FormatFloat(f, 'f', -1, 64)
|
|
}
|