gaoluyang
10 天以前 cbadfd8e691ab335965202a984db16e55ac8e42c
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -5,9 +5,17 @@
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.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.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;
@@ -24,6 +32,7 @@
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 +55,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;
@@ -88,6 +99,12 @@
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProMaterialRequirementService materialRequirementService;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Resource
    private ErpPurchaseRequestApi purchaseRequestApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -141,6 +158,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,22 +188,25 @@
    @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);
        if (MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) {
            updateObj.setStatus(MesProMpsStatusEnum.DRAFT.getStatus());
        }
        mpsMapper.updateById(updateObj);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteMps(Long id) {
        // 校验存在 + 草稿状态
        MesProMpsDO mps = validateMpsExistsAndDraft(id);
        // 校验存在 + 可编辑状态(未开始、审核中)
        MesProMpsDO mps = validateMpsExistsAndEditable(id);
        // 校验是否已关联工单
        if (mps.getWorkOrderId() != null) {
@@ -192,9 +219,98 @@
    @Override
    @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())) {
            throw exception(PRO_MPS_NOT_DRAFT);
        }
        mpsMapper.updateById(new MesProMpsDO()
                .setId(id)
                .setStatus(MesProMpsStatusEnum.AUDITING.getStatus()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void auditMps(MesProMpsAuditReqVO reqVO) {
        MesProMpsDO mps = validateMpsExists(reqVO.getId());
        if (!MesProMpsStatusEnum.AUDITING.getStatus().equals(mps.getStatus())) {
            throw exception(PRO_MPS_NOT_AUDITING);
        }
        boolean approved = Boolean.TRUE.equals(reqVO.getApproved());
        if (!approved && StrUtil.isBlank(reqVO.getAuditRemark())) {
            throw exception(PRO_MPS_AUDIT_REMARK_REQUIRED);
        }
        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
    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. 只对缺口物料生成采购明细:缺口 = 需求 - 可用库存 - 在途采购
        List<ErpPurchaseRequestCreateReqDTO.Item> shortageItems = new ArrayList<>();
        for (MesProWorkOrderItemRespVO item : getMpsItemList(id)) {
            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);
        }
        // 2. 生成采购申请单(草稿,无供应商,后续由采购人员补充并按流程提交)
        ErpPurchaseRequestCreateReqDTO createReqDTO = new ErpPurchaseRequestCreateReqDTO()
                .setRequestTime(LocalDateTime.now())
                .setRequestReason("生产计划 " + mps.getCode() + " 辅料采购")
                .setRemark("由生产计划自动生成,共 " + shortageItems.size() + " 项物料库存不足")
                .setItems(shortageItems);
        Long purchaseRequestId = purchaseRequestApi.createPurchaseRequest(createReqDTO).getCheckedData();
        // 3. 回写采购申请单编号,便于从计划追溯
        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 +339,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 +627,33 @@
    }
    /**
     * 校验 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())
                && !MesProMpsStatusEnum.AUDITING.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) {