高
3 天以前 c6a2b472cde9dff4d21b95637ee6d99a412c785c
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -1,22 +1,33 @@
package cn.iocoder.yudao.module.mes.service.qc.oqc;
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.module.mes.controller.admin.qc.oqc.vo.MesQcOqcImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo.MesQcOqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcImportRespVO;
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.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper;
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.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;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
@@ -31,7 +42,11 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -40,7 +55,7 @@
/**
 * MES 出货检验单(OQC) Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -48,6 +63,9 @@
    @Resource
    private MesQcOqcMapper oqcMapper;
    @Resource
    private MesQcImportHelper qcImportHelper;
    @Resource
    private MesQcTemplateItemService templateItemService;
@@ -73,6 +91,10 @@
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    @Lazy
    private MesQcNcrService ncrService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createOqc(MesQcOqcSaveReqVO createReqVO) {
@@ -81,9 +103,21 @@
        // 1.2 校验来源单据参数,并验证数据存在
        String sourceDocCode = validateAndGetSourceDocCode(
                createReqVO.getSourceDocType(), createReqVO.getSourceLineId());
        // 1.3 根据产品 + 检验类型自动匹配模板
        MesQcTemplateItemDO templateItem = templateItemService.getRequiredTemplateByItemIdAndType(
        // 1.3 根据产品 + OQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                createReqVO.getItemId(), MesQcTypeEnum.OQC.getType());
        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); // 指定了方案但不在匹配列表中,拒绝
            }
        }
        Long templateId = templateItem.getTemplateId();
        // 2. 插入主表
@@ -139,8 +173,34 @@
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());
        oqcMapper.updateById(updateObj);
        // 3. 回写来源单据
        // 3. 不合格时自动创建 NCR
        generateNcrIfNeeded(oqc);
        // 4. 回写来源单据
        writeBackSourceDoc(oqc);
    }
    /**
     * 不合格时自动创建 NCR(不合格品处理单)
     *
     * @param oqc 出货检验单
     */
    private void generateNcrIfNeeded(MesQcOqcDO oqc) {
        BigDecimal unqualifiedQty = oqc.getUnqualifiedQuantity();
        if (unqualifiedQty == null || unqualifiedQty.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        String ncrCode = "NCR" + java.time.LocalDateTime.now()
                .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
        MesQcNcrSaveReqVO saveReqVO = new MesQcNcrSaveReqVO();
        saveReqVO.setCode(ncrCode);
        saveReqVO.setName("不合格品处理单");
        saveReqVO.setQcType(MesQcTypeEnum.OQC.getType());
        saveReqVO.setQcId(oqc.getId());
        saveReqVO.setItemId(oqc.getItemId());
        saveReqVO.setUnqualifiedQuantity(unqualifiedQty);
        saveReqVO.setDefectDescription("由出货检验(" + oqc.getCode() + ")自动生成");
        ncrService.createNcr(saveReqVO);
    }
    /**
@@ -351,4 +411,124 @@
        return "物料ID:" + itemId;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesQcImportRespVO importOqcList(List<MesQcOqcImportExcelVO> importList) {
        MesQcImportRespVO respVO = MesQcImportRespVO.builder()
                .createList(new ArrayList<>()).failureList(new LinkedHashMap<>()).build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        qcImportHelper.startImport();
        Map<String, Integer> firstRowNoByKey = new HashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            MesQcOqcImportExcelVO 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(createOqcByImport(row));
                firstRowNoByKey.putIfAbsent(importKey, i + 2);
            } catch (Exception ex) {
                respVO.getFailureList().put(label, MesQcImportHelper.resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    private boolean isImportRowEmpty(MesQcOqcImportExcelVO row) {
        return row == null || (StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getName())
                && StrUtil.isBlank(row.getItemCode()) && StrUtil.isBlank(row.getClientName())
                && StrUtil.isBlank(row.getBatchCode()) && StrUtil.isBlank(row.getTemplateName())
                && row.getOutQuantity() == null && row.getCheckQuantity() == null
                && row.getQualifiedQuantity() == null && row.getUnqualifiedQuantity() == null
                && StrUtil.isBlank(row.getCheckResult()) && StrUtil.isBlank(row.getInspectorName())
                && StrUtil.isBlank(row.getInspectDate()) && StrUtil.isBlank(row.getOutDate())
                && StrUtil.isBlank(row.getRemark()));
    }
    /**
     * 依据导入行创建一张出货检验单:状态为「待检验」,检验行由质检方案自动生成
     *
     * 本分支的出货检验单不强制关联客户、不强制填写出货数量与出货日期,故这三列留空不报错。
     *
     * @return 检验单编号
     */
    private String createOqcByImport(MesQcOqcImportExcelVO row) {
        MesQcOqcSaveReqVO reqVO = new MesQcOqcSaveReqVO();
        // 1. 检验单编号:填写则使用,留空自动生成
        String code = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim()
                : qcImportHelper.generateCode(MesMdAutoCodeRuleCodeEnum.QC_OQC_CODE.getCode());
        if (oqcMapper.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()));
        Long clientId = qcImportHelper.resolveClientId(row.getClientName());
        if (clientId != null) {
            // 未填客户时不校验数据权限(单据不属于任何客户)
            qcImportHelper.validateClientPermission(clientId, row.getClientName().trim());
        }
        reqVO.setClientId(clientId);
        reqVO.setBatchCode(StrUtil.trimToNull(row.getBatchCode()));
        reqVO.setTemplateId(qcImportHelper.resolveTemplateId(row.getTemplateName()));
        // 3. 数量:合格品 + 不合格品 必须等于检测数量(检测数量留空则自动合计);出货数量可留空
        reqVO.setOutQuantity(qcImportHelper.resolveQuantity(row.getOutQuantity(), "本次出货数量", false));
        BigDecimal qualified = qcImportHelper.resolveQuantity(row.getQualifiedQuantity(), "合格品数量", true);
        BigDecimal unqualified = qcImportHelper.resolveQuantity(row.getUnqualifiedQuantity(), "不合格品数量", true);
        reqVO.setQualifiedQuantity(qualified);
        reqVO.setUnqualifiedQuantity(unqualified);
        reqVO.setCheckQuantity(qcImportHelper.resolveCheckQuantity(
                row.getCheckQuantity(), 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.setOutDate(qcImportHelper.parseDateTime(row.getOutDate(), "出货日期"));
        reqVO.setRemark(StrUtil.trimToNull(row.getRemark()));
        // 5. 去重:同一 物料 + 客户 + 批次号 + 出货日期 的出货检验单不可重复导入
        MesQcOqcDO duplicate = CollUtil.getFirst(oqcMapper.selectListByImportKey(reqVO.getItemId(), reqVO.getClientId(),
                reqVO.getBatchCode(), reqVO.getOutDate()));
        if (duplicate != null) {
            throw MesQcImportHelper.duplicateExistingException(buildImportKeyDesc(row), "出货检验单",
                    duplicate.getCode(), "质量管理-出货检验");
        }
        // 6. 复用创建逻辑:校验物料/人员、按物料匹配质检方案、生成检验行
        try {
            createOqc(reqVO);
        } catch (ServiceException ex) {
            MesQcImportHelper.rethrowTemplateError(ex, row.getItemCode(), "出货检验(OQC)");
        }
        return code;
    }
    /**
     * 导入去重键:同一 物料 + 客户 + 批次号 + 出货日期 视为同一批出货检验数据
     */
    private String buildImportKey(MesQcOqcImportExcelVO row) {
        return String.join("|", StrUtil.trimToEmpty(row.getItemCode()), StrUtil.trimToEmpty(row.getClientName()),
                StrUtil.trimToEmpty(row.getBatchCode()), StrUtil.trimToEmpty(row.getOutDate()));
    }
    private String buildImportKeyDesc(MesQcOqcImportExcelVO row) {
        return "物料[" + MesQcImportHelper.describeImportValue(row.getItemCode())
                + "]、客户[" + MesQcImportHelper.describeImportValue(row.getClientName())
                + "]、批次号[" + MesQcImportHelper.describeImportValue(row.getBatchCode())
                + "]、出货日期[" + MesQcImportHelper.describeImportValue(row.getOutDate()) + "]";
    }
}