云
4 天以前 f02d0df2f1dfc7a8b775cf50d45375e57628a5ef
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java
@@ -2,11 +2,19 @@
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.operatelog.core.util.OperateLogUtils;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcImportRespVO;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
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.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
@@ -28,6 +36,7 @@
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -42,6 +51,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.mes.enums.LogRecordConstants.*;
/**
 * MES 来料检验单(IQC) Service 实现类
@@ -51,6 +61,9 @@
@Service
@Validated
public class MesQcIqcServiceImpl implements MesQcIqcService {
    @Resource
    private cn.iocoder.yudao.module.mes.service.qc.MesQcCompletionHelper completionHelper;
    @Resource
    private MesQcIqcMapper iqcMapper;
@@ -83,6 +96,9 @@
    @Lazy
    private MesQcNcrService ncrService;
    @Resource
    private MesQcImportHelper qcImportHelper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIqc(MesQcIqcSaveReqVO createReqVO) {
@@ -91,33 +107,15 @@
        // 1.2 校验来源单据参数,并验证数据存在
        String sourceDocCode = validateAndGetSourceDocCode(
                createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId());
        // 1.3 根据产品 + IQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                createReqVO.getItemId(), MesQcTypeEnum.IQC.getType(), null, null);
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
        MesQcTemplateItemDO templateItem = templateItems.get(0);
        if (createReqVO.getTemplateId() != null) {
            templateItem = templateItems.stream()
                    .filter(item -> Objects.equals(item.getTemplateId(), createReqVO.getTemplateId()))
                    .findFirst().orElse(null);
            if (templateItem == null) {
                throw exception(QC_NO_TEMPLATE); // 指定了方案但不在匹配列表中,拒绝
            }
        }
        Long templateId = templateItem.getTemplateId();
        // 新建质检单不依赖质检方案。
        // 2. 插入主表
        MesQcIqcDO iqc = BeanUtils.toBean(createReqVO, MesQcIqcDO.class)
                .setTemplateId(templateId)
                .setTemplateId(null)
                .setCheckQuantity(createReqVO.getQualifiedQuantity().add(createReqVO.getUnqualifiedQuantity()))
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
        // 3. 从模板指标自动生成检验行
        iqcLineService.createLinesFromTemplate(iqc.getId(), templateId);
        return iqc.getId();
    }
@@ -138,11 +136,18 @@
    }
    private void validateIqcSaveData(MesQcIqcSaveReqVO reqVO) {
        // 空字符串也需要写入,以支持编辑时清空手填名称。
        if (reqVO.getVendorName() != null) {
            reqVO.setVendorName(reqVO.getVendorName().trim());
        }
        // 校验编号唯一
        validateIqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验供应商存在
        if (reqVO.getVendorId() != null) {
            srmSupplierApi.getSupplier(reqVO.getVendorId());
            var supplier = srmSupplierApi.getSupplier(reqVO.getVendorId()).getCheckedData();
            if (reqVO.getVendorName() == null && supplier != null) {
                reqVO.setVendorName(supplier.getName());
            }
        }
        // 校验物料存在(使用 MDM 物料)
        if (reqVO.getItemId() != null) {
@@ -155,17 +160,16 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishIqc(Long id) {
        completionHelper.lockDocument("iqc", id);
        // 1.1 校验存在 + 草稿状态
        MesQcIqcDO iqc = validateIqcStatusPrepare(id);
        if (iqc.getCheckResult() == null) {
            throw exception(QC_IQC_CHECK_RESULT_EMPTY);
        }
        // 1.2 校验至少存在一条检测结果
        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.IQC.getType());
        int result = completionHelper.validateAndGetResult("iqc", id, iqc.getQualifiedQuantity(), iqc.getUnqualifiedQuantity(), iqc.getCheckQuantity());
        iqc.setCheckResult(result);
        // 2. 更新状态为已完成
        MesQcIqcDO updateObj = new MesQcIqcDO()
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus()).setCheckResult(result);
        iqcMapper.updateById(updateObj);
        // 3. 不合格时自动创建 NCR
@@ -367,17 +371,6 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIqcFromArrivalNotice(Long noticeId, MesWmArrivalNoticeLineDO line, String noticeCode, Long vendorId) {
        // 1. 校验物料是否配置了 IQC 检验模板
        MesQcTemplateItemDO templateItem;
        try {
            templateItem = templateItemService.getRequiredTemplateByItemIdAndType(
                    line.getItemId(), MesQcTypeEnum.IQC.getType());
        } catch (Exception e) {
            // 获取物料信息,提供更明确的错误提示
            String itemInfo = getItemInfo(line.getItemId());
            throw exception(QC_NO_TEMPLATE_FOR_ITEM, itemInfo);
        }
        Long templateId = templateItem.getTemplateId();
        // 2. 生成检验单编码
        String code = "IQC" + System.currentTimeMillis();
@@ -386,12 +379,18 @@
        MesQcIqcDO iqc = new MesQcIqcDO();
        iqc.setCode(code);
        iqc.setName(noticeCode + "-物料检验");
        iqc.setTemplateId(templateId);
        iqc.setSourceDocType(MesBizTypeConstants.WM_ARRIVAL_NOTICE);
        iqc.setSourceDocId(noticeId);
        iqc.setSourceLineId(line.getId());
        iqc.setSourceDocCode(noticeCode);
        iqc.setVendorId(vendorId);
        if (vendorId != null) {
            var supplier = srmSupplierApi.getSupplier(vendorId).getCheckedData();
            if (supplier != null) {
                iqc.setVendorName(supplier.getName());
            }
        }
        iqc.setItemId(line.getItemId());
        iqc.setReceivedQuantity(line.getArrivalQuantity());
        iqc.setCheckQuantity(BigDecimal.ZERO); // 待填写
@@ -401,8 +400,6 @@
        iqc.setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
        // 4. 从模板指标自动生成检验行
        iqcLineService.createLinesFromTemplate(iqc.getId(), templateId);
        return iqc.getId();
    }
@@ -425,4 +422,121 @@
        return "物料ID:" + itemId;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = MES_QC_IQC_TYPE, subType = MES_QC_IQC_IMPORT_SUB_TYPE, bizNo = "0",
            success = MES_QC_IQC_IMPORT_SUCCESS)
    public MesQcImportRespVO importIqcList(List<MesQcIqcImportExcelVO> importList) {
        MesQcImportRespVO respVO = MesQcImportRespVO.builder()
                .createList(new ArrayList<>()).failureList(new LinkedHashMap<>()).build();
        if (CollUtil.isEmpty(importList)) {
            // 空文件也要给日志文案兜底,避免 {{#importSummary}} 取不到值
            OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList());
            return respVO;
        }
        qcImportHelper.startImport();
        Map<String, Integer> firstRowNoByKey = new HashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            MesQcIqcImportExcelVO row = importList.get(i);
            if (isImportRowEmpty(row)) {
                continue; // 跳过空行
            }
            String label = StrUtil.isNotBlank(row.getCode()) ? "检验单编号 " + row.getCode().trim()
                    : "第 " + (i + 2) + " 行";
            String importKey = buildImportKey(row);
            try {
                Integer firstRowNo = firstRowNoByKey.get(importKey);
                if (firstRowNo != null) {
                    throw MesQcImportHelper.duplicateRowException(firstRowNo, buildImportKeyDesc(row));
                }
                respVO.getCreateList().add(createIqcByImport(row));
                firstRowNoByKey.putIfAbsent(importKey, i + 2);
            } catch (Exception ex) {
                respVO.getFailureList().put(label, MesQcImportHelper.resolveImportErrorMessage(ex));
            }
        }
        // 记录导入结果汇总,供 @LogRecord 的 success 文案引用
        OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList());
        return respVO;
    }
    private boolean isImportRowEmpty(MesQcIqcImportExcelVO row) {
        return row == null || (StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getName())
                && StrUtil.isBlank(row.getItemCode()) && StrUtil.isBlank(row.getVendorName())
                && StrUtil.isBlank(row.getVendorBatch()) && StrUtil.isBlank(row.getTemplateName())
                && row.getReceivedQuantity() == null && row.getQualifiedQuantity() == null
                && row.getUnqualifiedQuantity() == null && StrUtil.isBlank(row.getCheckResult())
                && StrUtil.isBlank(row.getInspectorName()) && StrUtil.isBlank(row.getInspectDate())
                && StrUtil.isBlank(row.getReceiveDate()) && StrUtil.isBlank(row.getRemark()));
    }
    /**
     * 依据导入行创建一张来料检验单:状态为「待检验」,检验行由质检方案自动生成
     *
     * @return 检验单编号
     */
    private String createIqcByImport(MesQcIqcImportExcelVO row) {
        MesQcIqcSaveReqVO reqVO = new MesQcIqcSaveReqVO();
        // 1. 检验单编号:填写则使用,留空自动生成
        String code = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim()
                : qcImportHelper.generateCode(MesMdAutoCodeRuleCodeEnum.QC_IQC_CODE.getCode());
        if (iqcMapper.selectByCode(code) != null) {
            throw new IllegalArgumentException("检验单编号已存在:" + code
                    + "。请修改模板中的「检验单编号」,或先在系统中删除该编号的检验单后重新导入");
        }
        reqVO.setCode(code);
        // 2. 名称、物料、供应商、质检方案
        if (StrUtil.isBlank(row.getName())) {
            throw new IllegalArgumentException("检验单名称不能为空:请填写模板中该行的「检验单名称」列");
        }
        reqVO.setName(row.getName().trim());
        reqVO.setItemId(qcImportHelper.resolveItemId(row.getItemCode()));
        reqVO.setVendorName(StrUtil.trimToEmpty(row.getVendorName()));
        reqVO.setVendorBatch(StrUtil.trimToNull(row.getVendorBatch()));
        reqVO.setTemplateId(qcImportHelper.resolveTemplateId(row.getTemplateName()));
        // 3. 数量:合格品 + 不合格品 必须等于本次接收数量(接收数量留空则自动合计)
        BigDecimal qualified = qcImportHelper.resolveQuantity(row.getQualifiedQuantity(), "合格品数量", true);
        BigDecimal unqualified = qcImportHelper.resolveQuantity(row.getUnqualifiedQuantity(), "不合格品数量", true);
        reqVO.setQualifiedQuantity(qualified);
        reqVO.setUnqualifiedQuantity(unqualified);
        reqVO.setReceivedQuantity(qcImportHelper.resolveCheckQuantity(
                row.getReceivedQuantity(), qualified, unqualified, "本次接收数量"));
        // 4. 检验信息
        reqVO.setCheckResult(qcImportHelper.resolveDictValue(row.getCheckResult(),
                DictTypeConstants.MES_QC_CHECK_RESULT, "检测结果", false));
        reqVO.setInspectorUserId(qcImportHelper.resolveInspectorUserId(row.getInspectorName()));
        reqVO.setInspectDate(qcImportHelper.parseRequiredDateTime(row.getInspectDate(), "检测日期"));
        reqVO.setReceiveDate(qcImportHelper.parseRequiredDateTime(row.getReceiveDate(), "来料日期"));
        reqVO.setRemark(StrUtil.trimToNull(row.getRemark()));
        // 5. 去重:同一 物料 + 供应商 + 供应商批次 + 来料日期 的来料检验单不可重复导入
        MesQcIqcDO duplicate = CollUtil.getFirst(iqcMapper.selectListByImportKey(reqVO.getItemId(), reqVO.getVendorName(),
                reqVO.getVendorBatch(), reqVO.getReceiveDate()));
        if (duplicate != null) {
            throw MesQcImportHelper.duplicateExistingException(buildImportKeyDesc(row), "来料检验单",
                    duplicate.getCode(), "质量管理-来料检验");
        }
        // 6. 复用创建逻辑:校验物料/供应商/人员、按物料匹配质检方案、生成检验行
        try {
            createIqc(reqVO);
        } catch (ServiceException ex) {
            MesQcImportHelper.rethrowTemplateError(ex, row.getItemCode(), "来料检验(IQC)");
        }
        return code;
    }
    /**
     * 导入去重键:同一 物料 + 供应商 + 供应商批次 + 来料日期 视为同一批来料数据
     */
    private String buildImportKey(MesQcIqcImportExcelVO row) {
        return String.join("|", StrUtil.trimToEmpty(row.getItemCode()), StrUtil.trimToEmpty(row.getVendorName()),
                StrUtil.trimToEmpty(row.getVendorBatch()), StrUtil.trimToEmpty(row.getReceiveDate()));
    }
    private String buildImportKeyDesc(MesQcIqcImportExcelVO row) {
        return "物料[" + MesQcImportHelper.describeImportValue(row.getItemCode())
                + "]、供应商[" + MesQcImportHelper.describeImportValue(row.getVendorName())
                + "]、供应商批次[" + MesQcImportHelper.describeImportValue(row.getVendorBatch())
                + "]、来料日期[" + MesQcImportHelper.describeImportValue(row.getReceiveDate()) + "]";
    }
}