feat: 重构BOM架构,新增基础设置与虚拟工位管理功能
本次重构删除原有BOM物料清单表,改用工单工艺组合×工艺物料清单作为唯一用料来源;新增系统基础设置表支持WMS地址、日志保留天数等配置,新增虚拟工位作业管理后台接口与前端页签控制功能,同时优化工位号校验逻辑、事件日志自动清理与工单用料查询能力。
This commit is contained in:
@@ -79,26 +79,6 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
|
||||
}
|
||||
}
|
||||
}
|
||||
// BOM 物料未被任何涉及工艺引用 → 告警不阻断(工艺物料清单 ⊆ 产品 BOM 的兜底提醒)
|
||||
if bom, e := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault("")); e == nil {
|
||||
covered := map[string]bool{}
|
||||
for _, fms := range g.fmByFlow {
|
||||
for _, fm := range fms {
|
||||
covered[fm.MaterialCode] = true
|
||||
}
|
||||
}
|
||||
un := []string{}
|
||||
for _, b := range bom {
|
||||
if b.MaterialCode != "" && !covered[b.MaterialCode] {
|
||||
un = append(un, b.MaterialCode)
|
||||
}
|
||||
}
|
||||
if len(un) > 0 {
|
||||
s.ctx.EventLog.Write(ctx, "bom.unassigned", plan.OrderNo, operator, "product_bom", wo.ProductCode,
|
||||
"BOM 物料未配置在任何涉及工艺的物料清单,本次备料未覆盖",
|
||||
map[string]any{"materials": un})
|
||||
}
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
@@ -200,9 +180,9 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
|
||||
|
||||
// SyncOrderLedger 计算并向 WMS 下发某工单的「工单级物料需求总量」(需求10)。
|
||||
//
|
||||
// 需求量口径(临时.md §四 工单域):需求量 = 工单数量 × 单台用量 × (1+损耗率),向上取整。
|
||||
// 需求量口径(临时.md §四 工单域):需求量 = 工单数量 × 单台含损耗用量,向上取整。
|
||||
// 工单级而非日级——用日级会误卡跨天生产(临时.md L283-285)。
|
||||
// 多级 BOM 展开到叶子后按物料编码聚合(同编码合并),一个物料一行下发。
|
||||
// 用料唯一来源 = 工单工艺组合×工艺物料清单(BOM 已删除),同物料合并,一个物料一行下发。
|
||||
// WMS 侧按 orderNo+materialCode 幂等 upsert total_qty,可重复调用。
|
||||
func (s *Service) SyncOrderLedger(ctx context.Context, orderNo, operator string) error {
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Query().
|
||||
@@ -210,38 +190,20 @@ func (s *Service) SyncOrderLedger(ctx context.Context, orderNo, operator string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
leaves := s.expandBomToLeaf(ctx, wo.ProductCode, bomNameOrDefault(""), 1, 0, map[string]bool{})
|
||||
if len(leaves) == 0 {
|
||||
usage := s.WorkOrderUnitUsage(ctx, wo)
|
||||
if len(usage) == 0 {
|
||||
return nil
|
||||
}
|
||||
type agg struct {
|
||||
name string
|
||||
qty float64
|
||||
}
|
||||
m := map[string]*agg{}
|
||||
order := make([]string, 0, len(leaves))
|
||||
for _, it := range leaves {
|
||||
if it.materialCode == "" {
|
||||
continue
|
||||
}
|
||||
need := float64(wo.Quantity) * it.unitQty * (1 + it.lossRate/100)
|
||||
items := make([]wmsclient.LedgerSyncItem, 0, len(usage))
|
||||
for _, it := range usage {
|
||||
need := float64(wo.Quantity) * it.EffQty
|
||||
if need <= 0 {
|
||||
continue
|
||||
}
|
||||
if a, ok := m[it.materialCode]; ok {
|
||||
a.qty += need
|
||||
} else {
|
||||
m[it.materialCode] = &agg{name: it.materialName, qty: need}
|
||||
order = append(order, it.materialCode)
|
||||
}
|
||||
}
|
||||
items := make([]wmsclient.LedgerSyncItem, 0, len(order))
|
||||
for _, code := range order {
|
||||
a := m[code]
|
||||
items = append(items, wmsclient.LedgerSyncItem{
|
||||
MaterialCode: code,
|
||||
MaterialName: a.name,
|
||||
TotalQty: int(math.Ceil(a.qty - 1e-9)),
|
||||
MaterialCode: it.MaterialCode,
|
||||
MaterialName: it.MaterialName,
|
||||
TotalQty: int(math.Ceil(need - 1e-9)),
|
||||
})
|
||||
}
|
||||
if len(items) == 0 {
|
||||
@@ -362,29 +324,6 @@ func (s *Service) AutoOutboundRequests(ctx context.Context, planDate, orderNo, o
|
||||
return done, short
|
||||
}
|
||||
|
||||
// bomLeaf 递归展开后的叶子物料行(原材料/外购件)
|
||||
type bomLeaf struct {
|
||||
materialCode string
|
||||
materialName string
|
||||
unit string
|
||||
manageMode string
|
||||
unitQty float64 // 已折算父级倍率的单台用量
|
||||
lossRate float64
|
||||
}
|
||||
|
||||
// expandBomToLeaf 递归展开产品物料清单到叶子(原材料/外购件),供备料算料。
|
||||
//
|
||||
// 业务口径:成品可以作为零部件被另一个成品再次加工(多级嵌套)。备料算料时必须把
|
||||
// 成品子项按其自身的物料清单继续展开,直到原材料/外购件,否则产线会缺料。
|
||||
// 用量按倍率相乘(父级单台用量 × 子级单台用量)。
|
||||
//
|
||||
// R1(2026-09-19):递归骨架统一到 bom_expand.go 的 expandBomTree,与追溯侧
|
||||
// (expandBindBom)共用同一份实现;下钻判定由 wmsFinishedResolver 注入,
|
||||
// WMS 不可达时按叶子处理,不阻塞备料生成。
|
||||
func (s *Service) expandBomToLeaf(ctx context.Context, productCode, bomName string, mult float64, depth int, path map[string]bool) []bomLeaf {
|
||||
return bomTreeToLeaf(s.expandBomTree(ctx, productCode, bomName, mult, depth, path, s.wmsFinishedResolver))
|
||||
}
|
||||
|
||||
// splitQtyToStations 把日总量 dayQty 拆分到 stations 各工位:
|
||||
// 优先使用 stationQty 显式分配(其和>0 时直接采用);否则平均分配(余数补到前面工位)。
|
||||
func splitQtyToStations(stations []int, dayQty int, stationQty map[int]int) []int {
|
||||
|
||||
Reference in New Issue
Block a user