refactor: 重构工艺工序相关命名与数据模型

1.  全局替换"工序"为"工艺"统一术语,包括页面文案、枚举、注释
2.  重构工单与工件工艺数据模型:
    - 新增工艺组合表flow_material作为备料/齐套唯一源头
    - 新增工位状态表station_state支持自主停单/恢复接单
    - 移除station_process派工表,改用工单工艺组合作为路线唯一源头
    - 替换processCode为flowId作为工艺关联标识
    - 重构工件当前进度字段为currentStationNo
3.  删除冗余的静态备份资源文件
4.  调整物料选择组件默认启用仅显示激活物料
5.  优化工单创建/编辑逻辑,新增工艺组合校验与保存
This commit is contained in:
SunYF
2026-09-22 11:32:29 +08:00
parent 932700ca65
commit d609ff8a9e
185 changed files with 8623 additions and 4501 deletions
+96 -87
View File
@@ -11,23 +11,24 @@ import (
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/flowmaterial"
"bj_power_mes/ent/materialrequest"
"bj_power_mes/ent/station"
"bj_power_mes/ent/workorder"
"bj_power_mes/internal/wmsclient"
)
// GenerateMaterialRequest 按日排产 + 今日工位派工(station_process) 自动算料,生成工位级备料单。
// GenerateMaterialRequest 按日排产 + 工单工艺组合 + 工艺物料清单(flow_material) 自动算料,生成工位级备料单。
//
// 算法:
// - 路线来源:今日 station_process 派工(工位号→工序编号)。某工序(1~12)今日派工到哪些工位,
// 该工序的 BOM 物料就拆到这些工位各生成一行备料单(物料随工序上料)。
// - 数量:单工位量 = 该工位分配产量(日排产 stationPlanQty,未配置则日总量平均到各派工工位)
// × BOM 单台用量 ×(1+损耗率);同工序多工位时日总量按分配/平均拆分,不重复放大
// - 目标接驳台:取该派工工位的 station.dockCode(无接驳台则留空待仓管指定)。
// - processCode=0 的 BOM 物料(不参与工位绑定):按日总量生成一行,工位/接驳台留空
// 算法2026-09-22 生产模型定稿)
// - 路线来源:工单 flow_combination[{flowId,stationNo}],工位号即顺序号)。无组合的工单跳过并记日志。
// - 物料来源:组合条目涉及工艺的 flow_material(工艺物料清单 ⊆ 产品 BOM)。
// - 数量:备料 = 组合条目 × flow_material × 分产量——各工位量取日排产 stationPlanQty
// (全有值且和>0 时采用,否则同工艺工位平均拆、余数补前位);reqQty = 分产量 × 单台用量 ×(1+损耗率)
// - 目标接驳台:取 station.dock_code(无接驳台则留空待仓管指定)。
// - BOM 中未被任何涉及工艺引用的物料:记 eventlog(bom.unassigned) 告警,不阻断生成
//
// 生成前校验:BOM 内所有物料编码须在 WMS 物料档案存在,有缺失返回 missing(不生成);
// 生成前校验:涉及物料编码须在 WMS 物料档案存在,有缺失返回 missing(不生成);
// WMS 不可达/异常时降级为"全部存在"并记 eventlog,避免 WMS 抖动阻塞产线。
// 返回值:count=本次生成条数;missing=WMS 中不存在的物料编码(非空时 count=0)。
func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string, operator string) (int, []string, error) {
@@ -38,35 +39,13 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
if len(plans) == 0 {
return 0, nil, errors.New("该日期没有排产计划")
}
// 今日工位派工:工位号 → 工序编号
procMap, err := RouteStationProcessCodeMap(ctx, s.ctx.EntClient, TodayStr(), "")
if err != nil {
return 0, nil, err
type planGroup struct {
plan *ent.DailyPlan
wo *ent.WorkOrder
items []FlowItem // 工单工艺组合(工位号升序)
fmByFlow map[int][]*ent.FlowMaterial // 工艺ID → 工艺物料清单
}
if len(procMap) == 0 {
return 0, nil, errors.New("今日未配置工位派工(station_process),无法生成备料单")
}
// 工序编号 → 工位号列表(反查,供 BOM 物料按工序拆到工位)
stationsByProcess := map[int][]int{}
stNos := make([]int, 0, len(procMap))
for stNo, pc := range procMap {
stationsByProcess[pc] = append(stationsByProcess[pc], stNo)
stNos = append(stNos, stNo)
}
// 工位号 → 接驳台编码
dockByStation := map[int]string{}
if sts, e := s.ctx.EntClient.Station.Query().Where(station.StationNoIn(stNos...)).All(ctx); e == nil {
for _, st := range sts {
dockByStation[st.StationNo] = st.DockCode
}
}
type planBom struct {
plan *ent.DailyPlan
wo *ent.WorkOrder
bom []*ent.BomItem
}
groups := make([]planBom, 0, len(plans))
groups := make([]planGroup, 0, len(plans))
codeSet := map[string]bool{}
for _, plan := range plans {
if plan.Status == "CANCELLED" || plan.Status == "DONE" {
@@ -77,22 +56,53 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
if err != nil {
continue
}
bom, err := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault(""))
if err != nil {
items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, wo)
if len(items) == 0 {
s.ctx.EventLog.Write(ctx, "material.request.skip", plan.OrderNo, operator, "material_request", plan.OrderNo,
"工单未配置工艺组合,本日跳过备料", map[string]any{"planDate": plan.PlanDate})
continue
}
if len(bom) == 0 {
continue
}
for _, item := range bom {
if item.MaterialCode != "" {
codeSet[item.MaterialCode] = true
g := planGroup{plan: plan, wo: wo, items: items, fmByFlow: map[int][]*ent.FlowMaterial{}}
for _, it := range items {
if _, done := g.fmByFlow[it.FlowId]; done {
continue
}
fms, err := s.ctx.EntClient.FlowMaterial.Query().
Where(flowmaterial.FlowId(it.FlowId)).All(ctx)
if err != nil {
continue
}
g.fmByFlow[it.FlowId] = fms
for _, fm := range fms {
if fm.MaterialCode != "" {
codeSet[fm.MaterialCode] = true
}
}
}
groups = append(groups, planBom{plan: plan, wo: wo, bom: bom})
// 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 {
return 0, nil, errors.New("该日期排产均无有效 BOM 物料,无法生成备料单")
return 0, nil, errors.New("该日期排产均无有效工单工艺组合,无法生成备料单")
}
// 阶段1:WMS 物料存在性校验(全部存在才生成)
codes := make([]string, 0, len(codeSet))
@@ -107,7 +117,7 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
} else if len(missing) > 0 {
return 0, missing, nil
}
// 阶段2:全部存在 → 按工位拆料生成
// 阶段2:全部存在 → 按 组合条目 × flow_material × 分产量 拆料生成
created := 0
tsBase := time.Now().Format("20060102150405")
seq := 1
@@ -119,53 +129,53 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
stationQty[n] = v
}
}
// 成品可作零部件:BOM 子项若为成品,递归展开到叶子(原材料/外购件)再算料
leaves := s.expandBomToLeaf(ctx, g.wo.ProductCode, bomNameOrDefault(""), 1, 0, map[string]bool{})
for _, item := range leaves {
pc := item.processCode // 该物料所属装配工序(1~12),0=不参与工位绑定
if pc == 0 {
reqQty := float64(g.plan.PlanQty) * item.unitQty * (1 + item.lossRate/100)
if reqQty <= 0 {
continue
}
reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq)
seq++
_ = s.ctx.EntClient.MaterialRequest.Create().
SetRequestNo(reqNo).SetOrderNo(g.plan.OrderNo).SetPlanDate(g.plan.PlanDate).
SetMaterialCode(item.materialCode).SetMaterialName(item.materialName).
SetUnit(item.unit).SetManageMode(item.manageMode).SetReqQty(reqQty).
SetStatus("PENDING").SetOperator(operator).Exec(ctx)
created++
continue
// 工艺ID → 该工艺涉及的全部工位(组合内升序)
stationsByFlow := map[int][]int{}
for _, it := range g.items {
stationsByFlow[it.FlowId] = append(stationsByFlow[it.FlowId], it.StationNo)
}
// 工位号 → 接驳台编码
dockByStation := map[int]string{}
if sts, e := s.ctx.EntClient.Station.Query().
Where(station.StationNoIn(RouteStationsOfWO(g.items)...)).All(ctx); e == nil {
for _, st := range sts {
dockByStation[st.StationNo] = st.DockCode
}
stations := stationsByProcess[pc]
if len(stations) == 0 {
// 该工序今日未派工到任何工位,本日无需上料(非缺料)
continue
}
for flowId, stations := range stationsByFlow {
fms := g.fmByFlow[flowId]
if len(fms) == 0 {
continue // 该工艺未配置物料清单 → 无料可备
}
// 分产量:stationPlanQty 全有值且和>0 直接采用,否则平均拆、余数补前位
qtys := splitQtyToStations(stations, g.plan.PlanQty, stationQty)
for i, stNo := range stations {
q := qtys[i]
if q <= 0 {
for _, fm := range fms {
if fm.MaterialCode == "" {
continue
}
reqQty := float64(q) * item.unitQty * (1 + item.lossRate/100)
if reqQty <= 0 {
continue
for i, stNo := range stations {
q := qtys[i]
if q <= 0 {
continue
}
reqQty := float64(q) * fm.UnitQty * (1 + fm.LossRate/100)
if reqQty <= 0 {
continue
}
reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq)
seq++
_ = s.ctx.EntClient.MaterialRequest.Create().
SetRequestNo(reqNo).SetOrderNo(g.plan.OrderNo).SetPlanDate(g.plan.PlanDate).
SetMaterialCode(fm.MaterialCode).SetMaterialName(fm.MaterialName).
SetUnit(fm.Unit).SetManageMode(fm.ManageMode).SetReqQty(reqQty).
SetStatus("PENDING").SetOperator(operator).
SetStationNo(stNo).SetFlowId(flowId).SetTargetDock(dockByStation[stNo]).Exec(ctx)
created++
}
reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq)
seq++
_ = s.ctx.EntClient.MaterialRequest.Create().
SetRequestNo(reqNo).SetOrderNo(g.plan.OrderNo).SetPlanDate(g.plan.PlanDate).
SetMaterialCode(item.materialCode).SetMaterialName(item.materialName).
SetUnit(item.unit).SetManageMode(item.manageMode).SetReqQty(reqQty).
SetStatus("PENDING").SetOperator(operator).
SetStationNo(stNo).SetProcessCode(pc).SetTargetDock(dockByStation[stNo]).Exec(ctx)
created++
}
}
}
s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产+工位派工自动生成工位级备料单", map[string]any{"planDate": planDate, "count": created})
s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产+工单工艺组合+工艺物料清单生成工位级备料单", map[string]any{"planDate": planDate, "count": created})
// 需求10:把每个工单的「工单级需求总量」同步给 WMS 台账(幂等,失败降级不阻塞)。
// 必须先同步 total_qty,后续自动出库才能在台账上累加 out_qty 并判定缺口/领料完结。
synced := map[string]bool{}
@@ -360,7 +370,6 @@ type bomLeaf struct {
manageMode string
unitQty float64 // 已折算父级倍率的单台用量
lossRate float64
processCode int
}
// expandBomToLeaf 递归展开产品物料清单到叶子(原材料/外购件),供备料算料。