| | |
| | | import cn.hutool.core.util.StrUtil; |
| | | 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.erp.api.sale.ErpSaleOrderApi; |
| | | import cn.iocoder.yudao.module.system.api.permission.PermissionApi; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO; |
| | | 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.mysql.pro.mps.MesProMpsMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMergeItemMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; |
| | |
| | | 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.md.autocode.MesMdAutoCodeRecordService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService; |
| | |
| | | /** |
| | | * MES 生产主计划(MPS)Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | @Resource |
| | | private MesProMpsMapper mpsMapper; |
| | | @Resource |
| | | private MesProMpsMergeItemMapper mergeItemMapper; |
| | | private ErpSaleOrderApi saleOrderApi; |
| | | @Resource |
| | | private PermissionApi permissionApi; |
| | | @Resource |
| | | private MesProWorkOrderService workOrderService; |
| | | @Resource |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createMps(Long saleOrderId, String saleOrderNo, Long saleOrderItemId, |
| | | Long mdmItemId, String productName, BigDecimal quantity, Long clientId, |
| | | String requestDate, String remark) { |
| | | String requestDate, String remark, String reelNo) { |
| | | // 1. 根据 MDM 物料 ID 查找 MES 产品 |
| | | cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO mesItem = itemService.getItemByMdmItemId(mdmItemId); |
| | | if (mesItem == null) { |
| | |
| | | mps.setSaleOrderId(saleOrderId); |
| | | mps.setSaleOrderNo(saleOrderNo); |
| | | mps.setSaleOrderItemId(saleOrderItemId); |
| | | mps.setReelNo(reelNo); |
| | | mps.setProductId(mesItem.getId()); // 使用 MES 产品 ID |
| | | mps.setQuantity(quantity); |
| | | mps.setQuantityProduced(BigDecimal.ZERO); |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long issueMps(Long id) { |
| | | // 校验存在 + 草稿状态 |
| | | MesProMpsDO mps = validateMpsExistsAndDraft(id); |
| | | MesProMpsDO mps = mpsMapper.selectByIdForUpdate(id); |
| | | if (mps == null) { |
| | | throw exception(PRO_MPS_NOT_EXISTS); |
| | | } |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_DRAFT); |
| | | } |
| | | validateIssuePermission(mps); |
| | | |
| | | // 创建生产工单 |
| | | Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity()); |
| | |
| | | return workOrderId; |
| | | } |
| | | |
| | | private void validateIssuePermission(MesProMpsDO mps) { |
| | | Long userId = SecurityFrameworkUtils.getLoginUserId(); |
| | | if (userId == null) { |
| | | throw exception(PRO_MPS_ISSUE_FORBIDDEN); |
| | | } |
| | | if (permissionApi.hasAnyRoles(userId, "super_admin")) { |
| | | return; |
| | | } |
| | | // 手工创建且没有销售订单的计划以计划创建人为准。 |
| | | String creator = mps.getCreator(); |
| | | if (mps.getSaleOrderId() != null) { |
| | | var order = saleOrderApi.getSaleOrder(mps.getSaleOrderId()).getCheckedData(); |
| | | creator = order != null ? order.getCreator() : null; |
| | | } |
| | | if (!String.valueOf(userId).equals(creator)) { |
| | | throw exception(PRO_MPS_ISSUE_FORBIDDEN); |
| | | } |
| | | } |
| | | |
| | | /** 旧合并下发入口已停用;历史合并记录仍由取消和查询逻辑处理。 */ |
| | | @Deprecated |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long mergeAndIssueMps(List<Long> mpsIds) { |
| | | // 校验 MPS 列表 |
| | | if (CollUtil.isEmpty(mpsIds) || mpsIds.size() < 2) { |
| | | throw exception(PRO_MPS_MERGE_AT_LEAST_TWO); |
| | | } |
| | | |
| | | List<MesProMpsDO> mpsList = mpsMapper.selectBatchIds(mpsIds); |
| | | if (mpsList.size() != mpsIds.size()) { |
| | | throw exception(PRO_MPS_NOT_EXISTS); |
| | | } |
| | | |
| | | // 校验:必须是相同产品、草稿状态 |
| | | Long productId = null; |
| | | for (MesProMpsDO mps : mpsList) { |
| | | if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) { |
| | | throw exception(PRO_MPS_NOT_DRAFT); |
| | | } |
| | | if (productId == null) { |
| | | productId = mps.getProductId(); |
| | | } else if (!productId.equals(mps.getProductId())) { |
| | | throw exception(PRO_MPS_MERGE_PRODUCT_MISMATCH); |
| | | } |
| | | } |
| | | |
| | | // 计算合并数量 |
| | | BigDecimal totalQuantity = mpsList.stream() |
| | | .map(MesProMpsDO::getQuantity) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | |
| | | // 取第一条 MPS 作为主记录,创建生产工单 |
| | | MesProMpsDO firstMps = mpsList.get(0); |
| | | Long workOrderId = createWorkOrderFromMps(firstMps, totalQuantity); |
| | | |
| | | // 生成合并批次号 |
| | | String mergeBatchNo = generateMergeBatchNo(); |
| | | |
| | | // 更新所有 MPS 状态,记录合并关系 |
| | | for (MesProMpsDO mps : mpsList) { |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(mps.getId()) |
| | | .setStatus(MesProMpsStatusEnum.ISSUED.getStatus()) |
| | | .setWorkOrderId(workOrderId) |
| | | .setMergeFlag(1) |
| | | .setMergeBatchNo(mergeBatchNo) |
| | | .setIssueDate(LocalDateTime.now())); |
| | | |
| | | // 记录合并明细 |
| | | MesProMpsMergeItemDO mergeItem = new MesProMpsMergeItemDO(); |
| | | mergeItem.setMergeBatchNo(mergeBatchNo); |
| | | mergeItem.setMpsId(mps.getId()); |
| | | mergeItem.setWorkOrderId(workOrderId); |
| | | mergeItem.setQuantity(mps.getQuantity()); |
| | | mergeItemMapper.insert(mergeItem); |
| | | } |
| | | |
| | | log.info("[mergeAndIssueMps] MPS 合并下发成功,mpsIds={}, workOrderId={}, totalQuantity={}", |
| | | mpsIds, workOrderId, totalQuantity); |
| | | return workOrderId; |
| | | throw exception(PRO_MPS_MERGE_ISSUE_DISABLED); |
| | | } |
| | | |
| | | @Override |
| | |
| | | throw exception(PRO_MPS_NOT_ISSUED); |
| | | } |
| | | |
| | | // 级联取消关联的生产工单 |
| | | // 级联取消关联的生产工单(跟随工单当前状态,且不误伤合并下发的共享工单) |
| | | if (mps.getWorkOrderId() != null) { |
| | | try { |
| | | workOrderService.cancelWorkOrder(mps.getWorkOrderId()); |
| | | log.info("[cancelMps] 级联取消生产工单,workOrderId={}", mps.getWorkOrderId()); |
| | | } catch (Exception e) { |
| | | // 工单可能已完成或已取消,记录日志但不阻止 MPS 取消 |
| | | log.warn("[cancelMps] 取消生产工单失败,workOrderId={},原因:{}", mps.getWorkOrderId(), e.getMessage()); |
| | | // 新增逻辑:若该工单有报工记录,则不允许取消 MPS |
| | | Long feedbackCount = feedbackMapper.selectCount(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId()); |
| | | if (feedbackCount != null && feedbackCount > 0) { |
| | | throw exception(PRO_MPS_WORK_ORDER_HAS_FEEDBACK); |
| | | } |
| | | |
| | | // 1. 统计该工单被多少条 MPS 引用:合并下发的多条 MPS 共享同一个工单 |
| | | Long refCount = mpsMapper.selectCount(MesProMpsDO::getWorkOrderId, mps.getWorkOrderId()); |
| | | if (refCount != null && refCount > 1) { |
| | | // 共享工单:只取消当前 MPS,不动工单 |
| | | log.info("[cancelMps] 生产工单({}) 被 {} 条 MPS 共用,跳过级联取消工单", |
| | | mps.getWorkOrderId(), refCount); |
| | | } else { |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(mps.getWorkOrderId()); |
| | | if (workOrder != null |
| | | && MesProWorkOrderStatusEnum.CONFIRMED.getStatus().equals(workOrder.getStatus())) { |
| | | // 只有"已确认"状态的工单才需要级联取消 |
| | | workOrderService.cancelWorkOrder(mps.getWorkOrderId()); |
| | | log.info("[cancelMps] 级联取消生产工单,workOrderId={}", mps.getWorkOrderId()); |
| | | } else { |
| | | // 工单已是草稿/已完成/已取消,无需级联取消,直接取消 MPS |
| | | log.info("[cancelMps] 生产工单({}) 状态为 {},无需级联取消", |
| | | mps.getWorkOrderId(), workOrder != null ? workOrder.getStatus() : "null"); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesProMpsDO> getMpsPage(MesProMpsPageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return mpsMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return mpsMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | * 判断单个 MPS 是否完成 |
| | | */ |
| | | private boolean isMpsFinished(MesProMpsDO mps) { |
| | | // 已取消的 MPS 视为终态,不再阻塞生产完成判断(与 checkCanCreateSalesNotice 的口径保持一致) |
| | | if (MesProMpsStatusEnum.CANCELED.getStatus().equals(mps.getStatus())) { |
| | | return true; |
| | | } |
| | | // 1. 已生产数量 >= 计划数量 |
| | | BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO); |
| | | if (quantityProduced.compareTo(mps.getQuantity()) < 0) { |
| | |
| | | mps.getId(), quantityProduced, mps.getQuantity()); |
| | | return false; |
| | | } |
| | | // 2. 检查工单下所有报工是否完成(确保质检完成) |
| | | // 2. 已审批待检的报工已计入生产进度,不以质检状态阻塞生产完成。 |
| | | if (mps.getWorkOrderId() != null) { |
| | | Long unfinishedCount = feedbackMapper.selectCount( |
| | | new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProFeedbackDO>() |
| | | .eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId()) |
| | | .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus())); |
| | | .notIn(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus(), |
| | | MesProFeedbackStatusEnum.UNCHECK.getStatus())); |
| | | log.debug("[isMpsFinished] MPS {} unfinished feedback count: {}", mps.getId(), unfinishedCount); |
| | | if (unfinishedCount > 0) { |
| | | return false; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成合并批次号 |
| | | */ |
| | | /** 已停用:旧合并下发批次号生成逻辑,仅供识别历史实现,不再调用。 */ |
| | | @Deprecated |
| | | private String generateMergeBatchNo() { |
| | | return "MERGE" + System.currentTimeMillis(); |
| | | } |
| | |
| | | } |
| | | Long routeId = routeProduct.getRouteId(); |
| | | |
| | | // 2. 校验工艺路线是否有工序 |
| | | List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(routeId); |
| | | // 2. 校验工艺路线是否有工序(按产品过滤其它产品的产出物料) |
| | | List<MesProRouteProcessDO> routeProcesses = routeProcessService |
| | | .getRouteProcessListByProductId(mps.getProductId()); |
| | | if (CollUtil.isEmpty(routeProcesses)) { |
| | | throw exception(PRO_ROUTE_ENABLE_NO_PROCESS); |
| | | } |
| | |
| | | workOrderReq.setOrderSourceType(MesProWorkOrderSourceTypeEnum.ORDER.getType()); |
| | | workOrderReq.setOrderSourceCode(mps.getSaleOrderNo()); |
| | | workOrderReq.setProductId(mps.getProductId()); |
| | | workOrderReq.setReelNo(mps.getReelNo()); |
| | | workOrderReq.setQuantity(quantity); |
| | | workOrderReq.setClientId(mps.getClientId()); |
| | | workOrderReq.setRequestDate(mps.getRequestDate() != null ? mps.getRequestDate() : LocalDateTime.now()); |
| | |
| | | .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)) { |
| | |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |