From f02d0df2f1dfc7a8b775cf50d45375e57628a5ef Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 10 十一月 2026 11:26:22 +0800
Subject: [PATCH] feat(ncr): 完善NCR处置联动逻辑与关闭校验
---
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java | 168 +++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 155 insertions(+), 13 deletions(-)
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
index 3f3bb69..e6b69ea 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
@@ -85,12 +85,29 @@
*/
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;
@@ -170,6 +187,11 @@
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());
@@ -185,7 +207,10 @@
@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);
}
@@ -194,8 +219,9 @@
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()
@@ -216,12 +242,13 @@
@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) {
@@ -230,10 +257,71 @@
// 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("鍑鸿揣璁╂宸插畬鎴愬師閿�鍞嚭搴撳崟鎷h揣鍙婂嚭搴�");
+ } 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);
@@ -462,7 +550,7 @@
// 璇ユ柟娉曚細鎶婃墍鏈夊紓甯哥粺涓�鍖呮垚 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()));
}
@@ -519,10 +607,12 @@
@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;
}
@@ -545,11 +635,38 @@
@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. 鏇存柊鍏抽棴淇℃伅
@@ -558,6 +675,7 @@
.setCloseUserId(getLoginUserId())
.setCloseDate(LocalDateTime.now())
.setCloseOpinion(closeOpinion)
+ .setReinspectionIpqcId(reinspectionIpqcId)
.setStatus(MesNcrStatusEnum.CLOSED.getStatus()));
}
@@ -572,11 +690,19 @@
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()));
}
@@ -595,6 +721,22 @@
}
}
+ 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 瀛楃涓�
*/
--
Gitblit v1.9.3