yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/workstation/vo/MesMdWorkstationSaveReqVO.java
@@ -27,8 +27,7 @@ @NotNull(message = "所在车间不能为空") private Long workshopId; @Schema(description = "工序编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "所属工序不能为空") @Schema(description = "工序编号(可选,未配置工序时工作站仍可正常使用)", example = "1") private Long processId; @Schema(description = "线边库编号", example = "1") yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderController.java
@@ -285,7 +285,10 @@ List<MesProWorkOrderProcessProgressVO> progressList = new ArrayList<>(); Map<Long, List<MesProTaskDO>> processTaskMap = convertMultiMap(tasks, MesProTaskDO::getProcessId); // 未配置工序的单工序任务 processId 为空,分组前先过滤,避免 null key 抛 NPE Map<Long, List<MesProTaskDO>> processTaskMap = convertMultiMap( tasks.stream().filter(task -> task.getProcessId() != null).toList(), MesProTaskDO::getProcessId); for (MesProWorkOrderProcessDO process : processes) { List<MesProTaskDO> pTasks = processTaskMap.getOrDefault(process.getProcessId(), Collections.emptyList()); yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/workstation/MesMdWorkstationServiceImpl.java
@@ -98,8 +98,10 @@ validateWorkstationNameUnique(id, reqVO.getName()); // 校验车间存在 validateWorkshopExists(reqVO.getWorkshopId()); // 校验工序存在 processService.validateProcessExistsAndEnable(reqVO.getProcessId()); // 校验工序存在(工序为可选项,未配置工序时工作站仍可正常使用) if (reqVO.getProcessId() != null) { processService.validateProcessExistsAndEnable(reqVO.getProcessId()); } // 处理仓库层级(未指定仓库时自动设置虚拟线边库) handleWarehouseHierarchy(reqVO); } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -311,7 +311,12 @@ // 2.1 查询工序的关键工序标识 + 检验标识(需在 uncheckQuantity 校验之前,因为质检工序允许 uncheckQuantity > 0) MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( feedback.getRouteId(), feedback.getProcessId()); boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag()); // 未配置工艺路线工序时,视为单工序生产,按最后一道工序更新工单产量 boolean noProcessChain = routeProcess == null && CollUtil.isEmpty(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(feedback.getWorkOrderId())); boolean keyFlag = routeProcess != null ? Boolean.TRUE.equals(routeProcess.getKeyFlag()) : noProcessChain; boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag()); // 2.2 非质检工序:合格品/不良品数量由质检确定;本工序不经过质检,报工未填写时默认全部为合格品 if (!checkFlag @@ -422,7 +427,9 @@ // 3. 更新订单的已生产数量 // 新增逻辑:只有在该笔订单最后一个工序才更新生产订单的已生产数量 MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()); if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) { // 未配置工序链路的单工序工单,报工即视为完成最后一道工序 boolean noProcessChain = lastProcess == null && feedback.getRouteId() == null; if (noProcessChain || (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId()))) { workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity()); // 4. 更新 MPS 的已生产数量(如果是销售订单来源) @@ -508,12 +515,7 @@ reqVO.setItemId(task.getItemId()); reqVO.setWorkstationId(task.getWorkstationId()); // 3. 校验工艺路线 + 工序配置有效 MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( task.getRouteId(), task.getProcessId()); if (routeProcess == null) { throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID); } // 3. 未配置工艺路线/工序时按单工序报工处理,不再强制校验路线工序配置 // 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0 // 合格品/不良品数量由质检确定(非质检工序在审批时默认全部为合格品) if (reqVO.getFeedbackQuantity() == null @@ -603,12 +605,7 @@ // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报工 validatePreviousProcessFinished(task); // 2. 校验工艺路线工序配置 MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( task.getRouteId(), task.getProcessId()); if (routeProcess == null) { throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID); } // 2. 未配置工艺路线/工序时按单工序报工处理,不再强制校验路线工序配置 // 3. 校验数量:只填报工数量,且必须 > 0(合格品/不良品数量由质检确定) if (reqVO.getFeedbackQuantity() == null || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -676,19 +676,12 @@ * 根据 MPS 创建生产工单 */ private Long createWorkOrderFromMps(MesProMpsDO mps, BigDecimal quantity) { // 1. 校验产品是否存在工艺路线 // 1. 查找产品工艺路线;未配置时不阻断,按单工序生产处理 MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(mps.getProductId()); if (routeProduct == null) { throw exception(PRO_ROUTE_NOT_EXISTS); } Long routeId = routeProduct.getRouteId(); // 2. 校验工艺路线是否有工序(按产品过滤其它产品的产出物料) List<MesProRouteProcessDO> routeProcesses = routeProcessService .getRouteProcessListByProductId(mps.getProductId()); if (CollUtil.isEmpty(routeProcesses)) { throw exception(PRO_ROUTE_ENABLE_NO_PROCESS); } Long routeId = routeProduct != null ? routeProduct.getRouteId() : null; List<MesProRouteProcessDO> routeProcesses = routeProduct != null ? routeProcessService.getRouteProcessListByProductId(mps.getProductId()) : Collections.emptyList(); // 3. 创建生产工单 MesProWorkOrderSaveReqVO workOrderReq = new MesProWorkOrderSaveReqVO(); @@ -705,46 +698,50 @@ Long workOrderId = workOrderService.createWorkOrder(workOrderReq); // 4. 更新工单工艺路线 workOrderMapper.updateById(new MesProWorkOrderDO().setId(workOrderId).setRouteId(routeId)); // 5. 保存工单工序(从工艺路线工序带入,含投料BOM) Map<Long, List<MesProRouteProductBomDO>> bomMap = routeProcessService.getRouteProcessBomMapByProductId(mps.getProductId()); List<MesProWorkOrderProcessSaveReqVO> processList = new ArrayList<>(); for (MesProRouteProcessDO rp : routeProcesses) { MesProWorkOrderProcessSaveReqVO process = new MesProWorkOrderProcessSaveReqVO(); process.setWorkOrderId(workOrderId); process.setProcessId(rp.getProcessId()); process.setSort(rp.getSort()); process.setKeyFlag(rp.getKeyFlag()); process.setCheckFlag(rp.getCheckFlag()); process.setBackflushFlag(rp.getBackflushFlag()); if (rp.getOutputItemId() != null && !rp.getOutputItemId().isBlank()) { process.setOutputItemIds(Arrays.stream(rp.getOutputItemId().split(",")) .map(String::trim).filter(s -> !s.isEmpty()) .map(Long::valueOf).collect(Collectors.toList())); } process.setColorCode(rp.getColorCode()); // 带入工艺路线产品配置的报工仓库 process.setWarehouseId(routeProduct.getWarehouseId()); process.setLocationId(routeProduct.getLocationId()); process.setAreaId(routeProduct.getAreaId()); // 带入工序投料BOM List<MesProRouteProductBomDO> routeBomItems = bomMap != null ? bomMap.get(rp.getProcessId()) : null; if (CollUtil.isNotEmpty(routeBomItems)) { List<MesProWorkOrderProcessSaveReqVO.BomItem> bomItems = new ArrayList<>(); for (MesProRouteProductBomDO bomDO : routeBomItems) { MesProWorkOrderProcessSaveReqVO.BomItem bomItem = new MesProWorkOrderProcessSaveReqVO.BomItem(); bomItem.setItemId(bomDO.getItemId()); bomItem.setQuantity(bomDO.getQuantity()); bomItem.setRemark(bomDO.getRemark()); bomItems.add(bomItem); } process.setBomItems(bomItems); } processList.add(process); // 4. 更新工单工艺路线(未配置路线时保持为空,按单工序生产) if (routeId != null) { workOrderMapper.updateById(new MesProWorkOrderDO().setId(workOrderId).setRouteId(routeId)); } workOrderProcessService.batchSaveWorkOrderProcess(processList); // 5. 保存工单工序(仅配置了工艺路线工序时带入,含投料BOM) if (CollUtil.isNotEmpty(routeProcesses)) { Map<Long, List<MesProRouteProductBomDO>> bomMap = routeProcessService.getRouteProcessBomMapByProductId(mps.getProductId()); List<MesProWorkOrderProcessSaveReqVO> processList = new ArrayList<>(); for (MesProRouteProcessDO rp : routeProcesses) { MesProWorkOrderProcessSaveReqVO process = new MesProWorkOrderProcessSaveReqVO(); process.setWorkOrderId(workOrderId); process.setProcessId(rp.getProcessId()); process.setSort(rp.getSort()); process.setKeyFlag(rp.getKeyFlag()); process.setCheckFlag(rp.getCheckFlag()); process.setBackflushFlag(rp.getBackflushFlag()); if (rp.getOutputItemId() != null && !rp.getOutputItemId().isBlank()) { process.setOutputItemIds(Arrays.stream(rp.getOutputItemId().split(",")) .map(String::trim).filter(s -> !s.isEmpty()) .map(Long::valueOf).collect(Collectors.toList())); } process.setColorCode(rp.getColorCode()); // 带入工艺路线产品配置的报工仓库 process.setWarehouseId(routeProduct.getWarehouseId()); process.setLocationId(routeProduct.getLocationId()); process.setAreaId(routeProduct.getAreaId()); // 带入工序投料BOM List<MesProRouteProductBomDO> routeBomItems = bomMap != null ? bomMap.get(rp.getProcessId()) : null; if (CollUtil.isNotEmpty(routeBomItems)) { List<MesProWorkOrderProcessSaveReqVO.BomItem> bomItems = new ArrayList<>(); for (MesProRouteProductBomDO bomDO : routeBomItems) { MesProWorkOrderProcessSaveReqVO.BomItem bomItem = new MesProWorkOrderProcessSaveReqVO.BomItem(); bomItem.setItemId(bomDO.getItemId()); bomItem.setQuantity(bomDO.getQuantity()); bomItem.setRemark(bomDO.getRemark()); bomItems.add(bomItem); } process.setBomItems(bomItems); } processList.add(process); } workOrderProcessService.batchSaveWorkOrderProcess(processList); } // 6. 工单直接确认为已确认状态 workOrderService.confirmWorkOrder(workOrderId); yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -138,19 +138,18 @@ scheduleGuard.lock(); // 1. 校验关联数据存在 MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(createReqVO.getWorkOrderId()); var station = workstationService.validateWorkstationExistsAndEnable(createReqVO.getWorkstationId()); if (!java.util.Objects.equals(station.getProcessId(), createReqVO.getProcessId())) { throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_SCHEDULE_INVALID, "工作站与工序不匹配"); } routeService.validateRouteExists(createReqVO.getRouteId()); processService.validateProcessExistsAndEnable(createReqVO.getProcessId()); workstationService.validateWorkstationExistsAndEnable(createReqVO.getWorkstationId()); // 2. 自动确定 itemId:优先前端传入 → 工艺路线工序的产出产品 → 工单成品 // 未配置工序/工艺路线时,任务直接按工单成品产出 Long itemId = createReqVO.getItemId(); if (itemId == null) { if (itemId == null && createReqVO.getRouteId() != null && createReqVO.getProcessId() != null) { MesProRouteProcessDO routeProcess = routeProcessService .getRouteProcessByRouteIdAndProcessId(createReqVO.getRouteId(), createReqVO.getProcessId()); itemId = resolveTaskItemId(routeProcess, workOrder.getProductId()); } if (itemId == null) { itemId = workOrder.getProductId(); } MesMdItemDO item = itemService.validateItemExists(itemId); @@ -204,12 +203,6 @@ if (updateReqVO.getWorkstationId() != null) { workstationService.validateWorkstationExists(updateReqVO.getWorkstationId()); } if (updateReqVO.getRouteId() != null) { routeService.validateRouteExists(updateReqVO.getRouteId()); } if (updateReqVO.getProcessId() != null) { processService.validateProcessExistsAndEnable(updateReqVO.getProcessId()); } MesMdItemDO item; if (updateReqVO.getItemId() != null) { item = itemService.validateItemExists(updateReqVO.getItemId()); @@ -225,10 +218,6 @@ .setQuantity(quantity) .setStartTime(updateReqVO.getStartTime() != null ? updateReqVO.getStartTime() : task.getStartTime()) .setEndTime(updateReqVO.getEndTime() != null ? updateReqVO.getEndTime() : task.getEndTime()); var station = workstationService.validateWorkstationExistsAndEnable(candidate.getWorkstationId()); if (!java.util.Objects.equals(station.getProcessId(), candidate.getProcessId())) { throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_SCHEDULE_INVALID, "工作站与工序不匹配"); } scheduleGuard.validate(workOrderService.validateWorkOrderConfirmed(candidate.getWorkOrderId()), candidate, task.getId()); // 2. 更新 @@ -249,13 +238,24 @@ private void synchronizeScheduledQuantity(MesProWorkOrderDO order, Long routeId) { List<MesProRouteProcessDO> required = buildRouteProcessList(routeId, order.getId()); List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(order.getId()); BigDecimal scheduled = order.getQuantity(); for (var process : required) { BigDecimal amount = tasks.stream().filter(task -> java.util.Objects.equals(task.getProcessId(), process.getProcessId()) && !java.util.Objects.equals(task.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus())) BigDecimal orderQuantity = order.getQuantity() == null ? BigDecimal.ZERO : order.getQuantity(); BigDecimal scheduled; if (CollUtil.isEmpty(required)) { // 未配置工序/工艺路线:整张工单只有一个生产步骤,已排产量按任务数量合计 scheduled = tasks.stream() .filter(task -> !java.util.Objects.equals(task.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus())) .map(task -> task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity()) .reduce(BigDecimal.ZERO, BigDecimal::add); scheduled = scheduled.min(amount); .reduce(BigDecimal.ZERO, BigDecimal::add) .min(orderQuantity); } else { scheduled = orderQuantity; for (var process : required) { BigDecimal amount = tasks.stream().filter(task -> java.util.Objects.equals(task.getProcessId(), process.getProcessId()) && !java.util.Objects.equals(task.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus())) .map(task -> task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity()) .reduce(BigDecimal.ZERO, BigDecimal::add); scheduled = scheduled.min(amount); } } scheduleWorkOrderMapper.updateById(new MesProWorkOrderDO().setId(order.getId()).setQuantityScheduled(scheduled)); } @@ -340,7 +340,10 @@ } return result; } // 2. 回退到工艺路线模板工序 // 2. 回退到工艺路线模板工序;未配置工艺路线时视为单工序生产 if (routeId == null) { return new java.util.ArrayList<>(); } List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(routeId); if (CollUtil.isEmpty(routeProcesses)) { throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_ROUTE_ENABLE_NO_PROCESS);