| | |
| | | */ |
| | | private static final Set<Integer> DISPOSITION_ALLOWED_TYPES = Set.of( |
| | | MesNcrDispositionEnum.SCRAP.getType(), |
| | | MesNcrDispositionEnum.RETURN.getType(), |
| | | MesNcrDispositionEnum.REPRODUCTION.getType(), |
| | | MesNcrDispositionEnum.REWORK.getType(), |
| | | MesNcrDispositionEnum.CONCESSION.getType(), |
| | | MesNcrDispositionEnum.REPAIR.getType()); |
| | | |
| | | @Resource |
| | | private MesQcNcrMapper ncrMapper; |
| | | |
| | | @Resource |
| | | private MesQcNcrReinspectionValidator reinspectionValidator; |
| | | |
| | | @Resource |
| | | private MesQcNcrStockClosureGuard stockClosureGuard; |
| | | |
| | | @Resource private MesQcNcrScrapService scrapService; |
| | | @Resource private MesQcNcrReturnSalesLink returnSalesLink; |
| | | @Resource private MesQcPurchaseReturnService purchaseReturnService; |
| | | @Resource private MesQcPurchaseConcessionService purchaseConcessionService; |
| | | @Resource private MesQcNcrOqcSourceClaim oqcSourceClaim; |
| | | @Resource private MesQcNcrOqcConcessionService oqcConcessionService; |
| | | @Resource private MesQcNcrOqcReproductionService oqcReproductionService; |
| | | @Resource private MesQcNcrOqcReworkService oqcReworkService; |
| | | |
| | | @Resource |
| | | private MdmItemApi mdmItemApi; |
| | |
| | | public void updateNcr(MesQcNcrSaveReqVO updateReqVO) { |
| | | // 1. 校验存在 + 只允许待评审状态编辑 |
| | | MesQcNcrDO ncr = validateNcrStatusPendingReview(updateReqVO.getId()); |
| | | if (ncr.getQcId() != null && (!Objects.equals(ncr.getItemId(), updateReqVO.getItemId()) |
| | | || ncr.getUnqualifiedQuantity() == null || updateReqVO.getUnqualifiedQuantity() == null |
| | | || ncr.getUnqualifiedQuantity().compareTo(updateReqVO.getUnqualifiedQuantity()) != 0)) { |
| | | throw new ServiceException(400, "来源质检单生成的不合格物料和数量不能修改"); |
| | | } |
| | | validateNcrCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); |
| | | validateItemExists(updateReqVO.getItemId()); |
| | | |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteNcr(Long id) { |
| | | // 校验存在 + 只允许删除待评审状态 |
| | | validateNcrStatusPendingReview(id); |
| | | MesQcNcrDO ncr = validateNcrStatusPendingReview(id); |
| | | if (ncr.getQcId() != null) { |
| | | throw new ServiceException(400, "来源质检单生成的不合格处理单不能删除"); |
| | | } |
| | | ncrMapper.deleteById(id); |
| | | } |
| | | |
| | |
| | | public void reviewNcr(Long id, Integer disposition, Long responsibleDeptId, |
| | | Integer defectLevel, String reviewOpinion) { |
| | | // 1. 校验存在 + 待评审状态 |
| | | validateNcrStatusPendingReview(id); |
| | | MesQcNcrDO ncr = validateNcrStatusPendingReview(id); |
| | | validateDisposition(disposition); |
| | | validateDispositionForSource(ncr, disposition); |
| | | |
| | | // 2. 更新评审信息 |
| | | MesQcNcrDO updateObj = new MesQcNcrDO() |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesQcNcrDisposeRespVO disposeNcr(MesQcNcrDisposeReqVO disposeReqVO) { |
| | | // 1. 校验存在 + 已评审(审批通过)状态 |
| | | MesQcNcrDO ncr = validateNcrExists(disposeReqVO.getId()); |
| | | MesQcNcrDO ncr = validateNcrExistsForUpdate(disposeReqVO.getId()); |
| | | if (!MesNcrStatusEnum.REVIEWED.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_DISPOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | | // 2. 处置方式必填,且必须是合法枚举值 |
| | | validateDisposition(disposeReqVO.getDisposition()); |
| | | validateDispositionForSource(ncr, disposeReqVO.getDisposition()); |
| | | // 3. 不合格数量必须大于 0,否则无法生成入库单 / 生产订单 |
| | | if (ncr.getUnqualifiedQuantity() == null |
| | | || ncr.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | |
| | | |
| | | // 4. 按处置方式联动生成业务单据 |
| | | MesQcNcrDisposeRespVO respVO; |
| | | if (Objects.equals(MesNcrDispositionEnum.CONCESSION.getType(), disposeReqVO.getDisposition()) |
| | | || Objects.equals(MesNcrDispositionEnum.SCRAP.getType(), disposeReqVO.getDisposition())) { |
| | | // 让步放行 / 报废:生成杂项入库单并自动完成入库 |
| | | MesQcNcrScrapService.IpqcSource processSource = null; |
| | | MesQcNcrOqcSourceClaim.OqcClaimedSource oqcSource = null; |
| | | if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())) { |
| | | oqcSource = oqcSourceClaim.claim(ncr); |
| | | } |
| | | if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())) { |
| | | processSource = scrapService.validateIpqcSource(ncr); |
| | | scrapService.claimIpqcSource(ncr, processSource); |
| | | if (StrUtil.isNotBlank(processSource.batchCode()) && StrUtil.isNotBlank(disposeReqVO.getBatchCode()) |
| | | && !Objects.equals(processSource.batchCode(), StrUtil.trim(disposeReqVO.getBatchCode()))) { |
| | | throw new ServiceException(400, "处置批次必须与过程检验原批次一致"); |
| | | } |
| | | if (StrUtil.isNotBlank(processSource.batchCode())) disposeReqVO.setBatchCode(processSource.batchCode()); |
| | | } |
| | | if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType())) { |
| | | String originalBatch = returnSalesLink.claim(ncr); |
| | | if (StrUtil.isNotBlank(originalBatch) && StrUtil.isNotBlank(disposeReqVO.getBatchCode()) |
| | | && !Objects.equals(originalBatch, StrUtil.trim(disposeReqVO.getBatchCode()))) { |
| | | throw new ServiceException(400, "处置批次必须与客户退货原批次一致"); |
| | | } |
| | | if (StrUtil.isNotBlank(originalBatch)) disposeReqVO.setBatchCode(originalBatch); |
| | | } |
| | | if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.RETURN.getType())) { |
| | | var doc = purchaseReturnService.createForNcr(ncr); |
| | | respVO = new MesQcNcrDisposeRespVO(); |
| | | respVO.setDisposition(disposeReqVO.getDisposition()); |
| | | respVO.setDocType("PURCHASE_RETURN"); |
| | | respVO.setDocId(doc.getId()); |
| | | respVO.setDocCode(doc.getCode()); |
| | | respVO.setDocName("采购退货单"); |
| | | respVO.setMessage("采购退货单已生成,请由仓库确认退货"); |
| | | } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REPRODUCTION.getType())) { |
| | | respVO = processSource != null |
| | | ? oqcReproductionService.reproduceIpqc(ncr, disposeReqVO.getDisposition(), processSource) |
| | | : oqcReproductionService.reproduce(ncr, disposeReqVO.getDisposition()); |
| | | } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType()) |
| | | && Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())) { |
| | | respVO = purchaseConcessionService.dispose(ncr, disposeReqVO); |
| | | } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType()) |
| | | && Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType())) { |
| | | oqcConcessionService.release(ncr, disposeReqVO); |
| | | respVO = new MesQcNcrDisposeRespVO(); |
| | | respVO.setDisposition(disposeReqVO.getDisposition()); |
| | | respVO.setDocType("PRODUCT_SALES"); |
| | | MesQcOqcDO sourceOqc = oqcService.getOqc(ncr.getQcId()); |
| | | respVO.setDocId(sourceOqc.getSourceDocId()); |
| | | respVO.setDocCode(sourceOqc.getSourceDocCode()); |
| | | respVO.setDocName("销售出库单"); |
| | | respVO.setMessage("出货让步已完成原销售出库单拣货及出库"); |
| | | } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.SCRAP.getType()) |
| | | && (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType()) |
| | | || Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType()))) { |
| | | respVO = scrapService.receive(ncr, disposeReqVO); |
| | | } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType()) |
| | | && (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType()) |
| | | || Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType()))) { |
| | | respVO = createMiscReceiptForDispose(ncr, disposeReqVO); |
| | | } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType()) |
| | | || Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.SCRAP.getType())) { |
| | | stockClosureGuard.validateBeforeDispose(disposeReqVO.getDisposition()); |
| | | throw new ServiceException(400, "该来源暂不支持此处置方式"); |
| | | } else if (oqcSource != null && (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REWORK.getType()) |
| | | || Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REPAIR.getType()))) { |
| | | // 出货返工/返修:扣减原成品库存 → 取消原销售出库单 → 克隆返工工单 |
| | | respVO = oqcReworkService.dispose(ncr, oqcSource, disposeReqVO); |
| | | } else { |
| | | // 返工 / 返修:生成自行生产类型的生产订单 |
| | | respVO = createReworkWorkOrderForDispose(ncr, disposeReqVO); |
| | |
| | | // 该方法会把所有异常统一包成 RuntimeException,因此这里不能直接 catch ServiceException, |
| | | // 必须沿 cause 链判断是否为「流程定义不存在」。 |
| | | // 1. 校验存在 + 待评审(草稿/审批驳回后)状态 |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | MesQcNcrDO ncr = validateNcrExistsForUpdate(id); |
| | | if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_SUBMIT_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateNcrAuditStatus(Long id, Integer bpmResult) { |
| | | public void updateNcrAuditStatus(Long id, String processInstanceId, Integer bpmResult) { |
| | | // 1. 校验存在 + 审批中(避免并发或重复回调覆盖已处置/已关闭状态) |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus())) { |
| | | MesQcNcrDO ncr = validateNcrExistsForUpdate(id); |
| | | if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus()) |
| | | || StrUtil.isBlank(processInstanceId) |
| | | || !Objects.equals(processInstanceId, ncr.getProcessInstanceId())) { |
| | | return; |
| | | } |
| | | |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void closeNcr(Long id, String closeOpinion) { |
| | | public void closeNcr(Long id, String closeOpinion, Long reinspectionIpqcId) { |
| | | // 1. 校验存在 + 已处置状态 |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | MesQcNcrDO ncr = validateNcrExistsForUpdate(id); |
| | | if (!MesNcrStatusEnum.DISPOSED.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_CLOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | | |
| | | if (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.RETURN.getType()) |
| | | && Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())) { |
| | | purchaseReturnService.validateConfirmed(ncr.getId()); |
| | | } else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType()) |
| | | && Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())) { |
| | | oqcConcessionService.validateBeforeClose(ncr); |
| | | } else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType()) |
| | | && Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.CONCESSION.getType())) { |
| | | // 采购让步的入库和 ERP 回写与处置在同一事务完成。 |
| | | } else if (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REPRODUCTION.getType())) { |
| | | throw new ServiceException(400, "重新生产后需完成复检并处理原出库单,暂不能关闭"); |
| | | } else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType()) |
| | | && (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REWORK.getType()) |
| | | || Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REPAIR.getType()))) { |
| | | oqcReworkService.validateBeforeClose(ncr); |
| | | } else if (!Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType()) |
| | | && !Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType())) { |
| | | stockClosureGuard.validateBeforeClose(ncr); |
| | | } |
| | | |
| | | if (Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REWORK.getType()) |
| | | || Objects.equals(ncr.getDisposition(), MesNcrDispositionEnum.REPAIR.getType())) { |
| | | reinspectionValidator.validate(ncr, reinspectionIpqcId); |
| | | } else { |
| | | reinspectionIpqcId = null; |
| | | } |
| | | |
| | | // 2. 更新关闭信息 |
| | |
| | | .setCloseUserId(getLoginUserId()) |
| | | .setCloseDate(LocalDateTime.now()) |
| | | .setCloseOpinion(closeOpinion) |
| | | .setReinspectionIpqcId(reinspectionIpqcId) |
| | | .setStatus(MesNcrStatusEnum.CLOSED.getStatus())); |
| | | } |
| | | |
| | |
| | | return ncr; |
| | | } |
| | | |
| | | private MesQcNcrDO validateNcrExistsForUpdate(Long id) { |
| | | MesQcNcrDO ncr = ncrMapper.selectByIdForUpdate(id); |
| | | if (ncr == null) { |
| | | throw exception(QC_NCR_NOT_EXISTS, id); |
| | | } |
| | | return ncr; |
| | | } |
| | | |
| | | /** |
| | | * 校验 NCR 存在且为待评审状态 |
| | | */ |
| | | private MesQcNcrDO validateNcrStatusPendingReview(Long id) { |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | MesQcNcrDO ncr = validateNcrExistsForUpdate(id); |
| | | if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_STATUS_NOT_PENDING, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private void validateDispositionForSource(MesQcNcrDO ncr, Integer disposition) { |
| | | if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IQC.getType())) { |
| | | if (!Objects.equals(disposition, MesNcrDispositionEnum.RETURN.getType()) |
| | | && !Objects.equals(disposition, MesNcrDispositionEnum.CONCESSION.getType())) { |
| | | throw new ServiceException(400, "采购检验不合格仅支持退货或让步放行"); |
| | | } |
| | | } else if (Objects.equals(disposition, MesNcrDispositionEnum.RETURN.getType())) { |
| | | throw new ServiceException(400, "只有采购检验不合格支持采购退货"); |
| | | } |
| | | if (Objects.equals(disposition, MesNcrDispositionEnum.REPRODUCTION.getType()) |
| | | && !Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType()) |
| | | && !Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())) { |
| | | throw new ServiceException(400, "重新生产仅支持过程或出货检验不合格"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 状态值转名称,解析失败回退为 ID 字符串 |
| | | */ |