Files
bj_power/bj_power_mes/internal/logic/producible.go
T
SunYF 1cc93795ce feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
2026-09-17 12:49:07 +08:00

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)
}