| | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseRequestApi; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO; |
| | | import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestCreateReqDTO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsAuditReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPreparationCheckRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsMergeItemDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; |
| | | import cn.iocoder.yudao.module.mes.enums.dv.MesDvMachineryStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService; |
| | | import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.material.MesProMaterialRequirementService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService; |
| | |
| | | */ |
| | | private static final String MPS_CODE_RULE = "MPS"; |
| | | |
| | | /** |
| | | * MPS 审批流程定义 Key(BPM 流程模型中配置) |
| | | */ |
| | | private static final String MPS_APPROVE_PROCESS_DEFINITION_KEY = "mes-pro-mps-audit"; |
| | | |
| | | @Resource |
| | | private MesProMpsMapper mpsMapper; |
| | | @Resource |
| | |
| | | @Resource |
| | | private MesProMaterialRequirementService materialRequirementService; |
| | | @Resource |
| | | private MesProProcessService processService; |
| | | @Resource |
| | | private MesDvMachineryService machineryService; |
| | | @Resource |
| | | private ErpPurchaseOrderApi purchaseOrderApi; |
| | | @Resource |
| | | private ErpPurchaseRequestApi purchaseRequestApi; |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | // 校验编码唯一 |
| | | validateMpsCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); |
| | | |
| | | // 更新;审核中的计划被修改后需要重新提交审核 |
| | | // 更新(仅草稿可编辑) |
| | | MesProMpsDO updateObj = BeanUtils.toBean(updateReqVO, MesProMpsDO.class); |
| | | if (MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | updateObj.setStatus(MesProMpsStatusEnum.DRAFT.getStatus()); |
| | | } |
| | | mpsMapper.updateById(updateObj); |
| | | } |
| | | |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitMps(Long id) { |
| | | MesProMpsDO mps = validateMpsExists(id); |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus()) |
| | | && !MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | // 只有草稿可以提交审核;审核中的计划需等待协同办公审批结果 |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_DRAFT); |
| | | } |
| | | // 全局免审批:不做 BPM 流程,直接按审批通过处理 |
| | | if (processInstanceApi.isGlobalAutoApprove()) { |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(id) |
| | | .setStatus(MesProMpsStatusEnum.AUDITING.getStatus()) |
| | | .setProcessInstanceId("")); |
| | | handleBpmProcessResult(id, BpmProcessInstanceStatusEnum.APPROVE.getStatus(), "全局免审批自动通过", null); |
| | | return; |
| | | } |
| | | // 启动 BPM 流程,审批在协同办公完成,结果由监听器回写 |
| | | String processInstanceId; |
| | | try { |
| | | processInstanceId = processInstanceApi.createProcessInstance(SecurityFrameworkUtils.getLoginUserId(), |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(MPS_APPROVE_PROCESS_DEFINITION_KEY) |
| | | .setBusinessKey(String.valueOf(id))); |
| | | } catch (ServiceException ex) { |
| | | // 流程未部署时 BPM 只抛「流程定义不存在」,补上可执行的提示 |
| | | if (StrUtil.contains(ex.getMessage(), "流程定义不存在")) { |
| | | log.warn("[submitMps] 生产计划审批流程({}) 未部署,mpsId={}", MPS_APPROVE_PROCESS_DEFINITION_KEY, id); |
| | | throw exception(PRO_MPS_PROCESS_NOT_DEPLOYED); |
| | | } |
| | | throw ex; |
| | | } |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(id) |
| | | .setStatus(MesProMpsStatusEnum.AUDITING.getStatus())); |
| | | .setStatus(MesProMpsStatusEnum.AUDITING.getStatus()) |
| | | .setProcessInstanceId(processInstanceId)); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void auditMps(MesProMpsAuditReqVO reqVO) { |
| | | MesProMpsDO mps = validateMpsExists(reqVO.getId()); |
| | | public void handleBpmProcessResult(Long id, Integer bpmResult, String reason, Long auditUserId) { |
| | | // 1. 柔性校验:计划存在且处于审核中 |
| | | MesProMpsDO mps = mpsMapper.selectById(id); |
| | | if (mps == null) { |
| | | log.warn("[handleBpmProcessResult] 生产计划({}) 不存在", id); |
| | | return; |
| | | } |
| | | if (!MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_AUDITING); |
| | | log.warn("[handleBpmProcessResult] 生产计划({}) 不处于审核中状态,当前状态:{}", id, mps.getStatus()); |
| | | return; |
| | | } |
| | | boolean approved = Boolean.TRUE.equals(reqVO.getApproved()); |
| | | if (!approved && StrUtil.isBlank(reqVO.getAuditRemark())) { |
| | | throw exception(PRO_MPS_AUDIT_REMARK_REQUIRED); |
| | | |
| | | // 2. 根据审批结果更新状态 |
| | | if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) { |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(id) |
| | | .setStatus(MesProMpsStatusEnum.APPROVED.getStatus()) |
| | | .setAuditUserId(auditUserId) |
| | | .setAuditTime(LocalDateTime.now()) |
| | | .setAuditRemark(StrUtil.blankToDefault(reason, "审核通过"))); |
| | | } else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) |
| | | || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) { |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(id) |
| | | .setStatus(MesProMpsStatusEnum.DRAFT.getStatus()) |
| | | .setAuditUserId(auditUserId) |
| | | .setAuditTime(LocalDateTime.now()) |
| | | .setAuditRemark(StrUtil.blankToDefault(reason, "审批驳回"))); |
| | | } |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(reqVO.getId()) |
| | | .setStatus(approved ? MesProMpsStatusEnum.APPROVED.getStatus() |
| | | : MesProMpsStatusEnum.DRAFT.getStatus()) |
| | | .setAuditUserId(SecurityFrameworkUtils.getLoginUserId()) |
| | | .setAuditTime(LocalDateTime.now()) |
| | | .setAuditRemark(approved |
| | | ? StrUtil.blankToDefault(reqVO.getAuditRemark(), "审核通过") |
| | | : reqVO.getAuditRemark())); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | public MesProMpsPreparationCheckRespVO getPreparationCheck(Long id) { |
| | | MesProMpsDO mps = validateMpsExists(id); |
| | | MesProMpsPreparationCheckRespVO result = new MesProMpsPreparationCheckRespVO(); |
| | | result.setMpsId(mps.getId()); |
| | | result.setMpsCode(mps.getCode()); |
| | | result.setProductId(mps.getProductId()); |
| | | result.setQuantity(mps.getQuantity()); |
| | | MesMdItemDO product = itemService.getItem(mps.getProductId()); |
| | | result.setProductName(product != null ? product.getName() : String.valueOf(mps.getProductId())); |
| | | |
| | | // 1. 物料检查:需求 - 可用库存 - 在途采购 |
| | | List<MesProWorkOrderItemRespVO> materialItems = getMpsItemList(id); |
| | | int materialShortageCount = 0; |
| | | for (MesProWorkOrderItemRespVO item : materialItems) { |
| | | MesProMpsPreparationCheckRespVO.MaterialCheck check = new MesProMpsPreparationCheckRespVO.MaterialCheck(); |
| | | check.setItemId(item.getItemId()); |
| | | check.setItemCode(item.getItemCode()); |
| | | check.setItemName(item.getItemName()); |
| | | check.setSpecification(item.getItemSpecification()); |
| | | check.setUnitMeasureName(item.getUnitMeasureName()); |
| | | check.setRequiredQuantity(nullToZero(item.getQuantity())); |
| | | check.setStockQuantity(nullToZero(item.getStockQuantity())); |
| | | check.setPurchaseOrderQuantity(nullToZero(item.getPurchaseOrderQuantity())); |
| | | BigDecimal supply = nullToZero(item.getStockQuantity()).add(nullToZero(item.getPurchaseOrderQuantity())); |
| | | BigDecimal shortage = nullToZero(item.getQuantity()).subtract(supply); |
| | | if (shortage.compareTo(BigDecimal.ZERO) < 0) { |
| | | shortage = BigDecimal.ZERO; |
| | | } |
| | | check.setShortageQuantity(shortage); |
| | | check.setSufficient(shortage.compareTo(BigDecimal.ZERO) <= 0); |
| | | if (!Boolean.TRUE.equals(check.getSufficient())) { |
| | | materialShortageCount++; |
| | | } |
| | | result.getMaterials().add(check); |
| | | } |
| | | result.setMaterialReady(materialShortageCount == 0); |
| | | |
| | | // 2. 设备检查:按产品的工艺路线逐工序查找绑定设备 |
| | | List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByProductId(mps.getProductId()); |
| | | int equipmentUnavailableCount = 0; |
| | | if (CollUtil.isEmpty(routeProcesses)) { |
| | | MesProMpsPreparationCheckRespVO.EquipmentCheck check = new MesProMpsPreparationCheckRespVO.EquipmentCheck(); |
| | | check.setProcessName("工艺路线"); |
| | | check.setAvailable(false); |
| | | check.setReason("产品未配置工艺路线,无法确定所需设备;请先在「工艺路线」中维护该产品的工艺路线后再检查"); |
| | | result.getEquipments().add(check); |
| | | equipmentUnavailableCount++; |
| | | } else { |
| | | Map<Long, MesProRouteProcessDO> processIdMap = routeProcesses.stream() |
| | | .filter(rp -> rp.getProcessId() != null) |
| | | .collect(Collectors.toMap(MesProRouteProcessDO::getProcessId, rp -> rp, (a, b) -> a)); |
| | | Map<Long, MesProProcessDO> processMap = processService.getProcessMap(processIdMap.keySet()); |
| | | // 设备与工序/工位的绑定:优先取设备上的工序 ID,其次回退工位 ID(历史数据中两者一致) |
| | | List<MesDvMachineryDO> machineries = machineryService.getMachineryList(); |
| | | Map<Long, List<MesDvMachineryDO>> machineryMap = machineries.stream() |
| | | .filter(m -> m.getProcessId() != null || m.getWorkstationId() != null) |
| | | .collect(Collectors.groupingBy(m -> m.getProcessId() != null ? m.getProcessId() : m.getWorkstationId())); |
| | | for (MesProRouteProcessDO rp : routeProcesses) { |
| | | MesProProcessDO process = processMap.get(rp.getProcessId()); |
| | | String processName = process != null ? process.getName() : String.valueOf(rp.getProcessId()); |
| | | List<MesDvMachineryDO> boundMachineries = machineryMap.get(rp.getProcessId()); |
| | | if (CollUtil.isEmpty(boundMachineries)) { |
| | | MesProMpsPreparationCheckRespVO.EquipmentCheck check = new MesProMpsPreparationCheckRespVO.EquipmentCheck(); |
| | | check.setProcessId(rp.getProcessId()); |
| | | check.setProcessName(processName); |
| | | check.setAvailable(false); |
| | | check.setReason("该工序未绑定设备,请到「设备基础信息管理」为该工序绑定设备"); |
| | | result.getEquipments().add(check); |
| | | equipmentUnavailableCount++; |
| | | continue; |
| | | } |
| | | for (MesDvMachineryDO machinery : boundMachineries) { |
| | | MesProMpsPreparationCheckRespVO.EquipmentCheck check = new MesProMpsPreparationCheckRespVO.EquipmentCheck(); |
| | | check.setProcessId(rp.getProcessId()); |
| | | check.setProcessName(processName); |
| | | check.setMachineryId(machinery.getId()); |
| | | check.setMachineryCode(machinery.getCode()); |
| | | check.setMachineryName(machinery.getName()); |
| | | check.setStatus(machinery.getStatus()); |
| | | MesDvMachineryStatusEnum statusEnum = MesDvMachineryStatusEnum.valueOfStatus(machinery.getStatus()); |
| | | check.setStatusName(statusEnum != null ? statusEnum.getName() : "未知"); |
| | | boolean available = statusEnum != MesDvMachineryStatusEnum.MAINTENANCE; |
| | | check.setAvailable(available); |
| | | if (available) { |
| | | check.setReason(statusEnum == MesDvMachineryStatusEnum.PRODUCING ? "设备占用中,排产时需确认可用时段" : null); |
| | | } else { |
| | | check.setReason("设备处于维护中,请等待维护完成或改派其他设备"); |
| | | equipmentUnavailableCount++; |
| | | } |
| | | result.getEquipments().add(check); |
| | | } |
| | | } |
| | | } |
| | | result.setEquipmentReady(equipmentUnavailableCount == 0); |
| | | boolean ready = Boolean.TRUE.equals(result.getMaterialReady()) && Boolean.TRUE.equals(result.getEquipmentReady()); |
| | | result.setReady(ready); |
| | | StringBuilder summary = new StringBuilder(); |
| | | if (materialShortageCount > 0) { |
| | | summary.append(materialShortageCount).append(" 项物料短缺"); |
| | | } |
| | | if (equipmentUnavailableCount > 0) { |
| | | if (summary.length() > 0) { |
| | | summary.append(","); |
| | | } |
| | | summary.append(equipmentUnavailableCount).append(" 项设备不可用"); |
| | | } |
| | | if (summary.length() == 0) { |
| | | summary.append("物料、设备均已备齐,可以安排生产"); |
| | | } |
| | | result.setSummary(summary.toString()); |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long generatePurchaseRequest(Long id) { |
| | | MesProMpsDO mps = validateMpsExists(id); |
| | | if (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_APPROVED); |
| | | } |
| | | // 1. 只对缺口物料生成采购明细:缺口 = 需求 - 可用库存 - 在途采购 |
| | | // 1. 物料需求为空说明产品未维护任何 BOM,直接提示,避免误报“库存充足” |
| | | List<MesProWorkOrderItemRespVO> materialItems = getMpsItemList(id); |
| | | if (CollUtil.isEmpty(materialItems)) { |
| | | MesMdItemDO product = itemService.getItem(mps.getProductId()); |
| | | throw exception(PRO_MPS_NO_MATERIAL_BOM, mps.getCode(), |
| | | product != null ? product.getName() : String.valueOf(mps.getProductId())); |
| | | } |
| | | // 2. 只对缺口物料生成采购明细:缺口 = 需求 - 可用库存 - 在途采购 |
| | | List<ErpPurchaseRequestCreateReqDTO.Item> shortageItems = new ArrayList<>(); |
| | | for (MesProWorkOrderItemRespVO item : getMpsItemList(id)) { |
| | | for (MesProWorkOrderItemRespVO item : materialItems) { |
| | | if (Boolean.TRUE.equals(item.getStockSufficient())) { |
| | | continue; |
| | | } |
| | |
| | | if (CollUtil.isEmpty(shortageItems)) { |
| | | throw exception(PRO_MPS_NO_MATERIAL_SHORTAGE); |
| | | } |
| | | // 2. 生成采购申请单(草稿,无供应商,后续由采购人员补充并按流程提交) |
| | | // 3. 生成采购申请单(草稿,无供应商,后续由采购人员补充并按流程提交) |
| | | ErpPurchaseRequestCreateReqDTO createReqDTO = new ErpPurchaseRequestCreateReqDTO() |
| | | .setRequestTime(LocalDateTime.now()) |
| | | .setRequestReason("生产计划 " + mps.getCode() + " 辅料采购") |
| | | .setRemark("由生产计划自动生成,共 " + shortageItems.size() + " 项物料库存不足") |
| | | .setItems(shortageItems); |
| | | Long purchaseRequestId = purchaseRequestApi.createPurchaseRequest(createReqDTO).getCheckedData(); |
| | | // 3. 回写采购申请单编号,便于从计划追溯 |
| | | // 4. 回写采购申请单编号,便于从计划追溯 |
| | | mpsMapper.updateById(new MesProMpsDO().setId(id).setPurchaseRequestId(purchaseRequestId)); |
| | | log.info("[generatePurchaseRequest] 计划({}) 生成采购申请单({}),明细 {} 项", mps.getCode(), purchaseRequestId, shortageItems.size()); |
| | | return purchaseRequestId; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 校验 MPS 存在 + 可编辑状态(未开始、审核中) |
| | | * 校验 MPS 存在 + 可编辑状态(仅草稿;审核中的计划需等待协同办公审批) |
| | | */ |
| | | private MesProMpsDO validateMpsExistsAndEditable(Long id) { |
| | | MesProMpsDO mps = validateMpsExists(id); |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus()) |
| | | && !MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) { |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_DRAFT); |
| | | } |
| | | return mps; |
| | |
| | | */ |
| | | private String generateWorkOrderCode() { |
| | | try { |
| | | return autoCodeRecordService.generateAutoCode("WO"); |
| | | return autoCodeRecordService.generateAutoCode( |
| | | MesMdAutoCodeRuleCodeEnum.PRO_WORK_ORDER_CODE.getCode()); |
| | | } catch (Exception e) { |
| | | log.warn("[generateWorkOrderCode] 编码规则 WO 不存在,使用时间戳生成"); |
| | | log.warn("[generateWorkOrderCode] 编码规则 {} 不存在,使用时间戳生成", |
| | | MesMdAutoCodeRuleCodeEnum.PRO_WORK_ORDER_CODE.getCode()); |
| | | return "WO" + System.currentTimeMillis(); |
| | | } |
| | | } |