package logic import ( "context" "errors" "fmt" "strings" "time" "bj_power_mes/ent" "bj_power_mes/ent/materialrequest" "bj_power_mes/ent/workorder" ) // GenerateMaterialRequest 按日排产自动算料生成备料单 // 数量 = 该日所有工单计划数量 × 产品BOM单台用量×(1+损耗率),材料来源=该工单产品的 BOM // 生成前校验:BOM 内所有物料编码须在 WMS 物料档案存在,有缺失返回 missing(不生成); // WMS 不可达/异常时降级为"全部存在"并记 eventlog,避免 WMS 抖动阻塞产线。 // 返回值:count=本次生成条数;missing=WMS 中不存在的物料编码(非空时 count=0)。 func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string, operator string) (int, []string, error) { plans, err := s.ListDailyPlans(ctx, "", planDate) if err != nil { return 0, nil, err } if len(plans) == 0 { return 0, nil, errors.New("该日期没有排产计划") } type planBom struct { plan *ent.DailyPlan wo *ent.WorkOrder bom []*ent.BomItem } groups := make([]planBom, 0, len(plans)) codeSet := map[string]bool{} for _, plan := range plans { if plan.Status == "CANCELLED" || plan.Status == "DONE" { continue // 已取消/已完成的排产不再算料 } // 解析工单 → 产品编码 → 该产品 BOM wo, err := s.ctx.EntClient.WorkOrder.Query(). Where(workorder.WorkOrderNo(plan.OrderNo)).Only(ctx) if err != nil { continue } bom, err := s.ListBom(ctx, wo.ProductCode) if err != nil { continue } if len(bom) == 0 { continue } for _, item := range bom { if item.MaterialCode != "" { codeSet[item.MaterialCode] = true } } groups = append(groups, planBom{plan: plan, wo: wo, bom: bom}) } if len(groups) == 0 { return 0, nil, errors.New("该日期排产均无有效 BOM 物料,无法生成备料单") } // 阶段1:WMS 物料存在性校验(全部存在才生成) codes := make([]string, 0, len(codeSet)) for c := range codeSet { codes = append(codes, c) } missing, wmsErr := s.ctx.Wms.MaterialExists(ctx, codes) if wmsErr != nil { // 降级:WMS 不可达按"全部存在"放行,记日志供事后核查 s.ctx.EventLog.Write(ctx, "material.request.wms_skip", "", operator, "material_request", planDate, "WMS 物料校验不可达,已降级放行", map[string]any{"planDate": planDate, "error": wmsErr.Error()}) } else if len(missing) > 0 { return 0, missing, nil } // 阶段2:全部存在 → 算料生成 created := 0 // 备料单号基准:一次生成调用只取一次时间戳,序号全局递增,避免同一秒内多物料/多工单重复 tsBase := time.Now().Format("20060102150405") seq := 1 for _, g := range groups { for _, item := range g.bom { reqQty := float64(g.plan.PlanQty) * item.UnitQty * (1 + item.LossRate/100) if reqQty <= 0 { continue } reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq) seq++ // 送达接驳台:取该排产确认的 dock_codes(可多选,逗号分隔),未确认则为空串待仓管下发时指定 target := strings.Join(g.plan.DockCodes, ",") _ = 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). SetTargetDock(target).Exec(ctx) created++ } } s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产自动生成备料单", map[string]any{"planDate": planDate, "count": created}) s.notifyDashboard() return created, nil, nil } // ListMaterialRequests 查询备料单 func (s *Service) ListMaterialRequests(ctx context.Context, orderNo, planDate, status string) ([]*ent.MaterialRequest, error) { q := s.ctx.EntClient.MaterialRequest.Query() if orderNo != "" { q = q.Where(materialrequest.OrderNo(orderNo)) } if planDate != "" { q = q.Where(materialrequest.PlanDate(planDate)) } if status != "" { q = q.Where(materialrequest.Status(status)) } return q.Order(materialrequest.ByID()).All(ctx) } // SetMaterialRequestStatus 推进备料单状态(WMS 下发 AGV DELIVERING / 到位 DONE) func (s *Service) SetMaterialRequestStatus(ctx context.Context, requestNo, status string) error { n, err := s.ctx.EntClient.MaterialRequest.Update(). Where(materialrequest.RequestNo(requestNo)). SetStatus(status). Save(ctx) if err != nil { return err } if n == 0 { return errors.New("备料单不存在") } s.notifyDashboard() return nil }