gaoluyang
5 天以前 b31810d67d517b42c9f335d88affe0e9cc074ad7
fix(mes): 出货检验质检方案为空,未绑定模板时按产品类别兜底

- 出货检验方案查询:产品未绑定模板时,回退到启用的出货检验类方案
- 校验所选方案必须启用且检测种类包含出货检验,提示精确到方案名称
- 工时/模板项查询接口补充按质检种类过滤能力

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改4个文件
112 ■■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateItemController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/MesQcTemplateItemController.java
@@ -101,16 +101,16 @@
            @RequestParam(value = "taskId", required = false) Long taskId) {
        // 未指定物料/工单/任务时,按检测种类返回全部启用的质检方案(不限制产品物料关联)
        if (itemId == null && workOrderId == null && taskId == null && qcType != null) {
            List<MesQcTemplateItemOptionRespVO> options = templateService.getEnabledTemplateListByType(qcType)
                    .stream().map(template -> new MesQcTemplateItemOptionRespVO()
                            .setTemplateId(template.getId()).setTemplateCode(template.getCode())
                            .setTemplateName(template.getName()))
                    .collect(Collectors.toList());
            return success(options);
            return success(buildEnabledTemplateOptions(qcType));
        }
        List<MesQcTemplateItemDO> list = templateItemService.getTemplateItemList(
                itemId, qcType, workOrderId, taskId);
        if (CollUtil.isEmpty(list)) {
            // 该物料未绑定方案时,回退为该检测种类下的全部启用方案,供用户手动选择;
            // 否则无绑定的物料(如万通出货检验的绝大多数产品)下拉框永远为空,单据无法编制
            if (qcType != null && (itemId != null || workOrderId != null || taskId != null)) {
                return success(buildEnabledTemplateOptions(qcType));
            }
            return success(Collections.emptyList());
        }
        Map<Long, MesQcTemplateDO> templateMap = templateService.getTemplateMap(
@@ -125,6 +125,17 @@
        return success(result);
    }
    /**
     * 构建「该检测种类下全部启用方案」的选项列表
     */
    private List<MesQcTemplateItemOptionRespVO> buildEnabledTemplateOptions(Integer qcType) {
        return templateService.getEnabledTemplateListByType(qcType).stream()
                .map(template -> new MesQcTemplateItemOptionRespVO()
                        .setTemplateId(template.getId()).setTemplateCode(template.getCode())
                        .setTemplateName(template.getName()))
                .collect(Collectors.toList());
    }
    // ==================== 拼接 VO ====================
    private List<MesQcTemplateItemRespVO> buildItemRespVOList(List<MesQcTemplateItemDO> list) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -434,6 +434,8 @@
    ErrorCode QC_OQC_NO_TEMPLATE = new ErrorCode(1_040_607_005, "当前产品未配置 OQC 检测模板");
    ErrorCode QC_OQC_CHECK_RESULT_EMPTY = new ErrorCode(1_040_607_006, "完成检验单前,检测结果必须填写");
    ErrorCode QC_OQC_SOURCE_DOC_TYPE_UNKNOWN = new ErrorCode(1_040_607_007, "未知的 OQC 来源单据类型");
    ErrorCode QC_OQC_TEMPLATE_IS_DISABLE = new ErrorCode(1_040_607_008, "所选质检方案【{}】已禁用,请重新选择已启用的质检方案");
    ErrorCode QC_OQC_TEMPLATE_TYPE_NOT_MATCH = new ErrorCode(1_040_607_009, "所选质检方案【{}】的检测种类不包含出货检验,请重新选择检测种类包含出货检验的质检方案");
    // ========== MES 质量管理-出货检验行(1-040-607-100) ==========
    ErrorCode QC_OQC_LINE_NOT_EXISTS = new ErrorCode(1_040_607_100, "出货检验行不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -3,6 +3,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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;
@@ -15,6 +16,7 @@
import cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
@@ -24,6 +26,7 @@
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
@@ -33,6 +36,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
@@ -66,6 +70,8 @@
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Resource
    private MesQcTemplateService templateService;
    @Resource
    private MesQcOqcLineService oqcLineService;
    @Resource
@@ -106,19 +112,28 @@
        // 1.3 根据产品 + OQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                createReqVO.getItemId(), MesQcTypeEnum.OQC.getType(), null, null);
        Long templateId;
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
        MesQcTemplateItemDO templateItem = templateItems.get(0);
        if (createReqVO.getTemplateId() != null) {
            templateItem = templateItems.stream()
                    .filter(available -> Objects.equals(available.getTemplateId(), createReqVO.getTemplateId()))
                    .findFirst().orElse(null);
            if (templateItem == null) {
                throw exception(QC_NO_TEMPLATE); // 指定了方案但不在匹配列表中,拒绝
            // 产品物料未绑定出货检验方案:显式指定的方案改为校验方案本身(存在、已启用、检测种类含出货检验)
            if (createReqVO.getTemplateId() == null) {
                throw new ServiceException(QC_NO_TEMPLATE.getCode(), buildNoTemplateMessage(createReqVO.getItemId()));
            }
            templateId = validateTemplateUsable(createReqVO.getTemplateId());
        } else {
            MesQcTemplateItemDO templateItem = templateItems.get(0);
            if (createReqVO.getTemplateId() != null) {
                templateItem = templateItems.stream()
                        .filter(available -> Objects.equals(available.getTemplateId(), createReqVO.getTemplateId()))
                        .findFirst().orElse(null);
                if (templateItem == null) { // 指定了方案但不在该物料已绑定的匹配列表中,拒绝
                    throw new ServiceException(QC_NO_TEMPLATE.getCode(), StrUtil.format(
                            "所选质检方案不在产品物料[{}]已绑定的出货检验方案范围内:请重新选择质检方案,"
                                    + "或先在【质量管理-质检方案】中为该物料绑定所选方案后重试。",
                            describeItem(createReqVO.getItemId())));
                }
            }
            templateId = templateItem.getTemplateId();
        }
        Long templateId = templateItem.getTemplateId();
        // 2. 插入主表
        MesQcOqcDO oqc = BeanUtils.toBean(createReqVO, MesQcOqcDO.class)
@@ -147,6 +162,43 @@
        oqcMapper.updateById(updateObj);
    }
    /**
     * 校验质检方案本身可用:存在、已启用、检测种类包含出货检验,返回方案编号
     */
    private Long validateTemplateUsable(Long templateId) {
        MesQcTemplateDO template = templateService.getTemplate(templateId);
        if (template == null) {
            throw new ServiceException(QC_TEMPLATE_NOT_EXISTS.getCode(), StrUtil.format(
                    "所选质检方案不存在(ID={}),可能已被删除,请刷新后重新选择质检方案", templateId));
        }
        if (!CommonStatusEnum.isEnable(template.getStatus())) {
            throw exception(QC_OQC_TEMPLATE_IS_DISABLE, template.getName());
        }
        if (!CollUtil.contains(template.getTypes(), MesQcTypeEnum.OQC.getType())) {
            throw exception(QC_OQC_TEMPLATE_TYPE_NOT_MATCH, template.getName());
        }
        return template.getId();
    }
    /**
     * 构建「无可用质检方案」的提示:给出物料名称与下一步动作
     */
    private String buildNoTemplateMessage(Long itemId) {
        return StrUtil.format("产品物料[{}]未绑定「出货检验」质检方案,且未选择质检方案:请先选择质检方案后重试,"
                + "或在【质量管理-质检方案】中为该物料绑定检测种类包含出货检验的方案。", describeItem(itemId));
    }
    /**
     * 描述物料,用于异常提示:优先给出「编码 名称」,查不到时回退为物料编号
     */
    private String describeItem(Long itemId) {
        if (itemId == null) {
            return "未选择";
        }
        MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
        return item != null ? item.getCode() + " " + item.getName() : "物料编号 ID=" + itemId;
    }
    private void validateOqcSaveData(MesQcOqcSaveReqVO reqVO) {
        // 校验编号唯一
        validateOqcCodeUnique(reqVO.getId(), reqVO.getCode());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateItemServiceImpl.java
@@ -146,13 +146,22 @@
        if (resolvedItemId == null) {
            return Collections.emptyList();
        }
        // 2. mes_md_item.id 转 mdm_item.id(产品关联表存的是 mdm 物料)
        // 2. 解析成 mdm_item.id(产品关联表存的是 mdm 物料)
        //    - 生产任务/工单带出的产品编号一定是 mes_md_item.id,需要转换;
        //    - 前端直接选的物料(出货检验/退货检验用 MdmItemSelect)本身就是 mdm_item.id,
        //      mes_md_item 查不到(或已删除)时按 mdm_item.id 直接匹配,否则这些单据永远匹配不到方案
        Long mdmItemId = null;
        MesMdItemDO item = mesMdItemService.getItem(resolvedItemId);
        if (item == null || item.getMdmItemId() == null) {
        if (item != null && item.getMdmItemId() != null) {
            mdmItemId = item.getMdmItemId();
        } else if (taskId == null && workOrderId == null) {
            mdmItemId = resolvedItemId;
        }
        if (mdmItemId == null) {
            return Collections.emptyList();
        }
        // 3. 查出该物料关联的所有产品关联
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(item.getMdmItemId());
        List<MesQcTemplateItemDO> templateItems = templateItemMapper.selectListByItemId(mdmItemId);
        if (CollUtil.isEmpty(templateItems)) {
            return Collections.emptyList();
        }