| | |
| | | |
| | | 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; |
| | |
| | | import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.outsourcereceipt.MesWmOutsourceReceiptService; |
| | | 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.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; |
| | |
| | | |
| | | 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 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | @Resource |
| | | private MdmItemApi mdmItemApi; |
| | | |
| | | @Resource |
| | | @Lazy |
| | | private MesQcNcrService ncrService; |
| | | |
| | | @Resource |
| | | private MesQcImportHelper qcImportHelper; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createIqc(MesQcIqcSaveReqVO createReqVO) { |
| | |
| | | // 1.2 校验来源单据参数,并验证数据存在 |
| | | String sourceDocCode = validateAndGetSourceDocCode( |
| | | createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId()); |
| | | // 1.3 通过 itemId + IQC 类型自动查找模板 |
| | | MesQcTemplateItemDO templateItem = templateItemService.getRequiredTemplateByItemIdAndType( |
| | | createReqVO.getItemId(), MesQcTypeEnum.IQC.getType()); |
| | | // 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. 插入主表 |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus()); |
| | | iqcMapper.updateById(updateObj); |
| | | |
| | | // 3. 回写来源单据 |
| | | // 3. 不合格时自动创建 NCR |
| | | generateNcrIfNeeded(iqc); |
| | | |
| | | // 4. 回写来源单据 |
| | | writeBackSourceDoc(iqc); |
| | | } |
| | | |
| | | /** |
| | | * 不合格时自动创建 NCR(不合格品处理单) |
| | | * |
| | | * @param iqc 来料检验单 |
| | | */ |
| | | private void generateNcrIfNeeded(MesQcIqcDO iqc) { |
| | | BigDecimal unqualifiedQty = iqc.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.IQC.getType()); |
| | | saveReqVO.setQcId(iqc.getId()); |
| | | saveReqVO.setItemId(iqc.getItemId()); |
| | | saveReqVO.setUnqualifiedQuantity(unqualifiedQty); |
| | | saveReqVO.setDefectDescription("由来料检验(" + iqc.getCode() + ")自动生成"); |
| | | ncrService.createNcr(saveReqVO); |
| | | } |
| | | |
| | | /** |
| | |
| | | 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); // 待填写 |
| | |
| | | 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()) + "]"; |
| | | } |
| | | |
| | | } |