gaoluyang
6 天以前 3e8b8d3cb363b2a23e90e7f03e36472bfd27d696
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -3,11 +3,22 @@
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.MesProMpsPageReqVO;
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.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.mysql.pro.mps.MesProMpsMapper;
@@ -19,11 +30,13 @@
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.workorder.MesProWorkOrderDO;
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.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.pro.material.MesProMaterialRequirementService;
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;
@@ -46,6 +59,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -65,6 +80,11 @@
     * MPS 编码规则
     */
    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;
@@ -88,6 +108,14 @@
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProMaterialRequirementService materialRequirementService;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Resource
    private ErpPurchaseRequestApi purchaseRequestApi;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -141,6 +169,13 @@
    public Long createMps(MesProMpsSaveReqVO createReqVO) {
        // 校验产品存在
        itemService.validateItemExists(createReqVO.getProductId());
        // 计量单位未填写时,默认取产品单位
        if (createReqVO.getUnitMeasureId() == null) {
            MesMdItemDO product = itemService.getItem(createReqVO.getProductId());
            if (product != null) {
                createReqVO.setUnitMeasureId(product.getUnitMeasureId());
            }
        }
        // 如果未填写编码,自动生成
        if (StrUtil.isBlank(createReqVO.getCode())) {
@@ -164,13 +199,13 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateMps(MesProMpsSaveReqVO updateReqVO) {
        // 校验存在 + 草稿状态
        validateMpsExistsAndDraft(updateReqVO.getId());
        // 校验存在 + 可编辑状态(未开始、审核中)
        MesProMpsDO mps = validateMpsExistsAndEditable(updateReqVO.getId());
        // 校验编码唯一
        validateMpsCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        // 更新
        // 更新(仅草稿可编辑)
        MesProMpsDO updateObj = BeanUtils.toBean(updateReqVO, MesProMpsDO.class);
        mpsMapper.updateById(updateObj);
    }
@@ -178,8 +213,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteMps(Long id) {
        // 校验存在 + 草稿状态
        MesProMpsDO mps = validateMpsExistsAndDraft(id);
        // 校验存在 + 可编辑状态(未开始、审核中)
        MesProMpsDO mps = validateMpsExistsAndEditable(id);
        // 校验是否已关联工单
        if (mps.getWorkOrderId() != null) {
@@ -192,9 +227,141 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitMps(Long id) {
        MesProMpsDO mps = validateMpsExists(id);
        // 只有草稿可以提交审核;审核中的计划需等待协同办公审批结果
        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())
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    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())) {
            log.warn("[handleBpmProcessResult] 生产计划({}) 不处于审核中状态,当前状态:{}", id, mps.getStatus());
            return;
        }
        // 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, "审批驳回")));
        }
    }
    @Override
    public List<MesProWorkOrderItemRespVO> getMpsItemList(Long id) {
        MesProMpsDO mps = validateMpsExists(id);
        // 按产品 BOM(优先工艺路线 BOM)展开叶子物料,并按计划数量折算需求;
        // 计划尚未生成工单,无来源为本计划的在途采购,直接按可用库存判断是否充足
        return materialRequirementService.getItemListByProduct(mps.getProductId(), mps.getQuantity());
    }
    @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. 物料需求为空说明产品未维护任何 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 : materialItems) {
            if (Boolean.TRUE.equals(item.getStockSufficient())) {
                continue;
            }
            BigDecimal supply = nullToZero(item.getStockQuantity())
                    .add(nullToZero(item.getPurchaseOrderQuantity()));
            BigDecimal shortage = nullToZero(item.getQuantity()).subtract(supply);
            if (shortage.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            MesMdItemDO itemDO = itemService.getItem(item.getItemId());
            if (itemDO == null) {
                continue;
            }
            shortageItems.add(new ErpPurchaseRequestCreateReqDTO.Item()
                    .setProductId(item.getItemId())
                    .setProductUnitId(itemDO.getUnitMeasureId())
                    .setCount(shortage)
                    .setDemandTime(mps.getRequestDate() != null ? mps.getRequestDate().toLocalDate() : null)
                    .setRemark("生产计划 " + mps.getCode() + " 库存缺口 " + shortage.stripTrailingZeros().toPlainString()));
        }
        if (CollUtil.isEmpty(shortageItems)) {
            throw exception(PRO_MPS_NO_MATERIAL_SHORTAGE);
        }
        // 3. 生成采购申请单(草稿,无供应商,后续由采购人员补充并按流程提交)
        ErpPurchaseRequestCreateReqDTO createReqDTO = new ErpPurchaseRequestCreateReqDTO()
                .setRequestTime(LocalDateTime.now())
                .setRequestReason("生产计划 " + mps.getCode() + " 辅料采购")
                .setRemark("由生产计划自动生成,共 " + shortageItems.size() + " 项物料库存不足")
                .setItems(shortageItems);
        Long purchaseRequestId = purchaseRequestApi.createPurchaseRequest(createReqDTO).getCheckedData();
        // 4. 回写采购申请单编号,便于从计划追溯
        mpsMapper.updateById(new MesProMpsDO().setId(id).setPurchaseRequestId(purchaseRequestId));
        log.info("[generatePurchaseRequest] 计划({}) 生成采购申请单({}),明细 {} 项", mps.getCode(), purchaseRequestId, shortageItems.size());
        return purchaseRequestId;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long issueMps(Long id) {
        // 校验存在 + 草稿状态
        MesProMpsDO mps = validateMpsExistsAndDraft(id);
        // 校验存在 + 已审核状态
        MesProMpsDO mps = validateMpsExistsAndApproved(id);
        // 创建生产工单
        Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity());
@@ -223,11 +390,11 @@
            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 (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) {
                throw exception(PRO_MPS_NOT_APPROVED);
            }
            if (productId == null) {
                productId = mps.getProductId();
@@ -511,6 +678,32 @@
    }
    /**
     * 校验 MPS 存在 + 已审核状态
     */
    private MesProMpsDO validateMpsExistsAndApproved(Long id) {
        MesProMpsDO mps = validateMpsExists(id);
        if (!MesProMpsStatusEnum.APPROVED.getStatus().equals(mps.getStatus())) {
            throw exception(PRO_MPS_NOT_APPROVED);
        }
        return mps;
    }
    /**
     * 校验 MPS 存在 + 可编辑状态(仅草稿;审核中的计划需等待协同办公审批)
     */
    private MesProMpsDO validateMpsExistsAndEditable(Long id) {
        MesProMpsDO mps = validateMpsExists(id);
        if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
            throw exception(PRO_MPS_NOT_DRAFT);
        }
        return mps;
    }
    private static BigDecimal nullToZero(BigDecimal value) {
        return value != null ? value : BigDecimal.ZERO;
    }
    /**
     * 校验 MPS 编码唯一
     */
    private void validateMpsCodeUnique(Long id, String code) {
@@ -533,19 +726,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();
@@ -562,46 +748,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);
@@ -614,9 +804,11 @@
     */
    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();
        }
    }