Commit d415f7740533e5d7883506986f9f979e71d97b42

Authored by YunaiV
1 parent 80183a18

✨ feat(mes): 添加 IPQC 相关错误码和更新反馈处理逻辑

新增 IPQC 过程中的错误码,增强了对来源产出行 ID 的校验逻辑,确保在处理反馈时能够直接定位待检行。此变更提升了系统的健壮性和用户体验。
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -353,6 +353,8 @@ public interface ErrorCodeConstants { @@ -353,6 +353,8 @@ public interface ErrorCodeConstants {
353 ErrorCode QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN = new ErrorCode(1_040_604_007, "未知的 IPQC 来源单据类型"); 353 ErrorCode QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN = new ErrorCode(1_040_604_007, "未知的 IPQC 来源单据类型");
354 ErrorCode QC_IPQC_SOURCE_DOC_NO_PENDING_LINE = new ErrorCode(1_040_604_008, "来源报工单不存在待检产出行,无法创建 IPQC"); 354 ErrorCode QC_IPQC_SOURCE_DOC_NO_PENDING_LINE = new ErrorCode(1_040_604_008, "来源报工单不存在待检产出行,无法创建 IPQC");
355 ErrorCode QC_IPQC_SOURCE_LINE_NOT_BELONG = new ErrorCode(1_040_604_009, "来源单据行不属于该报工单"); 355 ErrorCode QC_IPQC_SOURCE_LINE_NOT_BELONG = new ErrorCode(1_040_604_009, "来源单据行不属于该报工单");
  356 + ErrorCode QC_IPQC_SOURCE_LINE_REQUIRED = new ErrorCode(1_040_604_010, "来源单据类型为报工时,来源产出行 ID 不能为空");
  357 + ErrorCode QC_IPQC_SOURCE_LINE_NOT_PENDING = new ErrorCode(1_040_604_011, "来源产出行不是待检验状态");
356 // ========== MES 质量管理-过程检验行(1-040-604-100) ========== 358 // ========== MES 质量管理-过程检验行(1-040-604-100) ==========
357 ErrorCode QC_IPQC_LINE_NOT_EXISTS = new ErrorCode(1_040_604_100, "过程检验行不存在"); 359 ErrorCode QC_IPQC_LINE_NOT_EXISTS = new ErrorCode(1_040_604_100, "过程检验行不存在");
358 360
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java
@@ -104,13 +104,15 @@ public interface MesProFeedbackService { @@ -104,13 +104,15 @@ public interface MesProFeedbackService {
104 * 并根据检验结果回写合格/不合格/废品数量,同时更新任务/工单的已生产数量。 104 * 并根据检验结果回写合格/不合格/废品数量,同时更新任务/工单的已生产数量。
105 * 105 *
106 * @param feedbackId 报工记录 ID 106 * @param feedbackId 报工记录 ID
  107 + * @param sourceLineId 来源产出行 ID(用于直接定位待检行)
107 * @param qualifiedQty 合格品数量 108 * @param qualifiedQty 合格品数量
108 * @param unqualifiedQty 不合格品数量 109 * @param unqualifiedQty 不合格品数量
109 * @param laborScrapQty 工废数量 110 * @param laborScrapQty 工废数量
110 * @param materialScrapQty 料废数量 111 * @param materialScrapQty 料废数量
111 * @param otherScrapQty 其他废品数量 112 * @param otherScrapQty 其他废品数量
112 */ 113 */
113 - void updateProFeedbackWhenIpqcFinish(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty, 114 + void updateProFeedbackWhenIpqcFinish(Long feedbackId, Long sourceLineId,
  115 + BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
114 BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty); 116 BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty);
115 117
116 } 118 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -313,7 +313,8 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { @@ -313,7 +313,8 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService {
313 313
314 @Override 314 @Override
315 @Transactional(rollbackFor = Exception.class) 315 @Transactional(rollbackFor = Exception.class)
316 - public void updateProFeedbackWhenIpqcFinish(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty, 316 + public void updateProFeedbackWhenIpqcFinish(Long feedbackId, Long sourceLineId,
  317 + BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
317 BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) { 318 BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
318 // 1. 校验报工单存在且为待检验状态 319 // 1. 校验报工单存在且为待检验状态
319 MesProFeedbackDO feedback = validateFeedbackExists(feedbackId); 320 MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
@@ -322,7 +323,7 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { @@ -322,7 +323,7 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService {
322 } 323 }
323 324
324 // 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库 325 // 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库
325 - productProduceService.splitPendingAndFinishProduce(feedbackId, qualifiedQty, unqualifiedQty); 326 + productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, qualifiedQty, unqualifiedQty);
326 327
327 // 3. 回写合格/不合格/废品数量,更新状态为已完成 328 // 3. 回写合格/不合格/废品数量,更新状态为已完成
328 feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId) 329 feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -221,7 +221,7 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService { @@ -221,7 +221,7 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService {
221 } 221 }
222 222
223 if (Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) { 223 if (Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) {
224 - feedbackService.updateProFeedbackWhenIpqcFinish(ipqc.getSourceDocId(), 224 + feedbackService.updateProFeedbackWhenIpqcFinish(ipqc.getSourceDocId(), ipqc.getSourceLineId(),
225 ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO), 225 ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO),
226 ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO), 226 ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO),
227 ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO), 227 ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO),
@@ -286,20 +286,19 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService { @@ -286,20 +286,19 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService {
286 } 286 }
287 if (Objects.equals(sourceDocType, MesBizTypeConstants.PRO_FEEDBACK)) { 287 if (Objects.equals(sourceDocType, MesBizTypeConstants.PRO_FEEDBACK)) {
288 MesProFeedbackDO feedback = feedbackService.validateFeedbackExists(sourceDocId); 288 MesProFeedbackDO feedback = feedbackService.validateFeedbackExists(sourceDocId);
289 - // 校验该报工存在待检产出行  
290 - List<MesWmProductProduceLineDO> lines = productProduceLineService  
291 - .getProductProduceLineListByFeedbackId(sourceDocId);  
292 - boolean hasPending = lines.stream().anyMatch(  
293 - l -> Objects.equals(l.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus()));  
294 - if (!hasPending) {  
295 - throw exception(QC_IPQC_SOURCE_DOC_NO_PENDING_LINE); 289 + // 校验 sourceLineId 必填
  290 + if (sourceLineId == null) {
  291 + throw exception(QC_IPQC_SOURCE_LINE_REQUIRED);
296 } 292 }
297 - // 校验 sourceLineId 属于该报工的产出行  
298 - if (sourceLineId != null) {  
299 - boolean belongs = lines.stream().anyMatch(l -> Objects.equals(l.getId(), sourceLineId));  
300 - if (!belongs) {  
301 - throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG);  
302 - } 293 + // 校验 sourceLineId 存在,且属于该报工的产出行
  294 + MesWmProductProduceLineDO targetLine = productProduceLineService
  295 + .validateProductProduceLineExists(sourceLineId);
  296 + if (ObjUtil.notEqual(targetLine.getFeedbackId(), sourceDocId)) {
  297 + throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG);
  298 + }
  299 + // 校验该产出行为待检验状态
  300 + if (ObjUtil.notEqual(targetLine.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) {
  301 + throw exception(QC_IPQC_SOURCE_LINE_NOT_PENDING);
303 } 302 }
304 return feedback.getCode(); 303 return feedback.getCode();
305 } 304 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceLineService.java
@@ -19,6 +19,14 @@ public interface MesWmProductProduceLineService { @@ -19,6 +19,14 @@ public interface MesWmProductProduceLineService {
19 void createProductProduceLine(MesWmProductProduceLineDO line); 19 void createProductProduceLine(MesWmProductProduceLineDO line);
20 20
21 /** 21 /**
  22 + * 校验生产入库单行存在
  23 + *
  24 + * @param id 行 ID
  25 + * @return 行数据
  26 + */
  27 + MesWmProductProduceLineDO validateProductProduceLineExists(Long id);
  28 +
  29 + /**
22 * 更新生产入库单行(内部使用) 30 * 更新生产入库单行(内部使用)
23 * 31 *
24 * @param line 行数据 32 * @param line 行数据
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceLineServiceImpl.java
@@ -10,6 +10,9 @@ import org.springframework.validation.annotation.Validated; @@ -10,6 +10,9 @@ import org.springframework.validation.annotation.Validated;
10 10
11 import java.util.List; 11 import java.util.List;
12 12
  13 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  14 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
  15 +
13 /** 16 /**
14 * MES 生产入库单行 Service 实现类 17 * MES 生产入库单行 Service 实现类
15 */ 18 */
@@ -26,6 +29,15 @@ public class MesWmProductProduceLineServiceImpl implements MesWmProductProduceLi @@ -26,6 +29,15 @@ public class MesWmProductProduceLineServiceImpl implements MesWmProductProduceLi
26 } 29 }
27 30
28 @Override 31 @Override
  32 + public MesWmProductProduceLineDO validateProductProduceLineExists(Long id) {
  33 + MesWmProductProduceLineDO line = productProduceLineMapper.selectById(id);
  34 + if (line == null) {
  35 + throw exception(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS);
  36 + }
  37 + return line;
  38 + }
  39 +
  40 + @Override
29 public void updateProductProduceLine(MesWmProductProduceLineDO line) { 41 public void updateProductProduceLine(MesWmProductProduceLineDO line) {
30 productProduceLineMapper.updateById(line); 42 productProduceLineMapper.updateById(line);
31 } 43 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceService.java
@@ -41,9 +41,11 @@ public interface MesWmProductProduceService { @@ -41,9 +41,11 @@ public interface MesWmProductProduceService {
41 * 待检产出({@code checkFlag=true})按检验结果拆分行,并执行入库。 41 * 待检产出({@code checkFlag=true})按检验结果拆分行,并执行入库。
42 * 42 *
43 * @param feedbackId 报工记录 ID 43 * @param feedbackId 报工记录 ID
  44 + * @param sourceLineId 来源产出行 ID(直接定位待检行)
44 * @param qualifiedQty 合格品数量 45 * @param qualifiedQty 合格品数量
45 * @param unqualifiedQty 不合格品数量 46 * @param unqualifiedQty 不合格品数量
46 */ 47 */
47 - void splitPendingAndFinishProduce(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty); 48 + void splitPendingAndFinishProduce(Long feedbackId, Long sourceLineId,
  49 + BigDecimal qualifiedQty, BigDecimal unqualifiedQty);
48 50
49 } 51 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
@@ -222,7 +222,8 @@ public class MesWmProductProduceServiceImpl implements MesWmProductProduceServic @@ -222,7 +222,8 @@ public class MesWmProductProduceServiceImpl implements MesWmProductProduceServic
222 222
223 @Override 223 @Override
224 @Transactional(rollbackFor = Exception.class) 224 @Transactional(rollbackFor = Exception.class)
225 - public void splitPendingAndFinishProduce(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty) { 225 + public void splitPendingAndFinishProduce(Long feedbackId, Long sourceLineId,
  226 + BigDecimal qualifiedQty, BigDecimal unqualifiedQty) {
226 // 1.1 查询产出单 227 // 1.1 查询产出单
227 MesWmProductProduceDO produce = productProduceMapper.selectByFeedbackId(feedbackId); 228 MesWmProductProduceDO produce = productProduceMapper.selectByFeedbackId(feedbackId);
228 if (produce == null) { 229 if (produce == null) {
@@ -233,13 +234,8 @@ public class MesWmProductProduceServiceImpl implements MesWmProductProduceServic @@ -233,13 +234,8 @@ public class MesWmProductProduceServiceImpl implements MesWmProductProduceServic
233 MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION); 234 MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
234 MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA); 235 MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
235 236
236 - // 2. 查找待检验行(checkFlag=true 时只有一行 PENDING)  
237 - List<MesWmProductProduceLineDO> lines = productProduceLineService.getProductProduceLineListByProduceId(produce.getId());  
238 - MesWmProductProduceLineDO pendingLine = CollUtil.findOne(lines,  
239 - l -> ObjUtil.equal(l.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus()));  
240 - if (pendingLine == null) {  
241 - throw exception(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS);  
242 - } 237 + // 2. 通过 sourceLineId 直接定位待检验行
  238 + MesWmProductProduceLineDO pendingLine = productProduceLineService.validateProductProduceLineExists(sourceLineId);
243 239
244 // 3A. 情况一:存在不合格品数量,需要拆分行 240 // 3A. 情况一:存在不合格品数量,需要拆分行
245 if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) { 241 if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java
@@ -61,6 +61,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { @@ -61,6 +61,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
61 // 准备数据:插入一条待检验状态的报工单 61 // 准备数据:插入一条待检验状态的报工单
62 Long taskId = randomLongId(); 62 Long taskId = randomLongId();
63 Long workOrderId = randomLongId(); 63 Long workOrderId = randomLongId();
  64 + Long sourceLineId = randomLongId();
64 MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> { 65 MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
65 o.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()); 66 o.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus());
66 o.setTaskId(taskId); 67 o.setTaskId(taskId);
@@ -87,13 +88,12 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { @@ -87,13 +88,12 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
87 BigDecimal laborScrapQty = BigDecimal.valueOf(5); 88 BigDecimal laborScrapQty = BigDecimal.valueOf(5);
88 BigDecimal materialScrapQty = BigDecimal.valueOf(10); 89 BigDecimal materialScrapQty = BigDecimal.valueOf(10);
89 BigDecimal otherScrapQty = BigDecimal.valueOf(5); 90 BigDecimal otherScrapQty = BigDecimal.valueOf(5);
90 -  
91 - feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), 91 + feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), sourceLineId,
92 qualifiedQty, unqualifiedQty, laborScrapQty, materialScrapQty, otherScrapQty); 92 qualifiedQty, unqualifiedQty, laborScrapQty, materialScrapQty, otherScrapQty);
93 93
94 // 断言 1:调用了 splitPendingAndFinishProduce 94 // 断言 1:调用了 splitPendingAndFinishProduce
95 verify(productProduceService).splitPendingAndFinishProduce( 95 verify(productProduceService).splitPendingAndFinishProduce(
96 - eq(feedback.getId()), eq(qualifiedQty), eq(unqualifiedQty)); 96 + eq(feedback.getId()), eq(sourceLineId), eq(qualifiedQty), eq(unqualifiedQty));
97 97
98 // 断言 2:报工单状态更新为已完成 98 // 断言 2:报工单状态更新为已完成
99 MesProFeedbackDO updatedFeedback = feedbackMapper.selectById(feedback.getId()); 99 MesProFeedbackDO updatedFeedback = feedbackMapper.selectById(feedback.getId());
@@ -137,7 +137,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { @@ -137,7 +137,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
137 .thenReturn(ListUtil.of(qualifiedLine)); 137 .thenReturn(ListUtil.of(qualifiedLine));
138 138
139 // 调用 139 // 调用
140 - feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), 140 + feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), randomLongId(),
141 BigDecimal.valueOf(50), BigDecimal.ZERO, 141 BigDecimal.valueOf(50), BigDecimal.ZERO,
142 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO); 142 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
143 143
@@ -156,7 +156,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { @@ -156,7 +156,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
156 // 调用不存在的 feedbackId,应该抛异常 156 // 调用不存在的 feedbackId,应该抛异常
157 Long feedbackId = randomLongId(); 157 Long feedbackId = randomLongId();
158 assertThrows(Exception.class, () -> 158 assertThrows(Exception.class, () ->
159 - feedbackService.updateProFeedbackWhenIpqcFinish(feedbackId, 159 + feedbackService.updateProFeedbackWhenIpqcFinish(feedbackId, randomLongId(),
160 BigDecimal.TEN, BigDecimal.ZERO, 160 BigDecimal.TEN, BigDecimal.ZERO,
161 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)); 161 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
162 } 162 }
@@ -171,12 +171,12 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { @@ -171,12 +171,12 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
171 171
172 // 调用,应该抛异常 172 // 调用,应该抛异常
173 assertThrows(Exception.class, () -> 173 assertThrows(Exception.class, () ->
174 - feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), 174 + feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), randomLongId(),
175 BigDecimal.TEN, BigDecimal.ZERO, 175 BigDecimal.TEN, BigDecimal.ZERO,
176 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)); 176 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
177 177
178 // 断言:不应该调用任何产出单方法 178 // 断言:不应该调用任何产出单方法
179 - verify(productProduceService, never()).splitPendingAndFinishProduce(anyLong(), any(), any()); 179 + verify(productProduceService, never()).splitPendingAndFinishProduce(anyLong(), anyLong(), any(), any());
180 } 180 }
181 181
182 @Test 182 @Test
@@ -189,7 +189,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { @@ -189,7 +189,7 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
189 189
190 // 调用,应该抛异常 190 // 调用,应该抛异常
191 assertThrows(Exception.class, () -> 191 assertThrows(Exception.class, () ->
192 - feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), 192 + feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), randomLongId(),
193 BigDecimal.TEN, BigDecimal.ZERO, 193 BigDecimal.TEN, BigDecimal.ZERO,
194 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)); 194 BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
195 } 195 }
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java
1 package cn.iocoder.yudao.module.mes.service.qc.ipqc; 1 package cn.iocoder.yudao.module.mes.service.qc.ipqc;
2 2
  3 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
3 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 4 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
4 import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO; 5 import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
5 import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO; 6 import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
  7 +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
6 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO; 8 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
7 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; 9 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
  10 +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
8 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; 11 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
9 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; 12 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
10 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO; 13 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
  14 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
11 import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; 15 import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
12 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; 16 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
13 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; 17 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
  18 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
14 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; 19 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
15 import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; 20 import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
16 import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService; 21 import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
@@ -19,7 +24,9 @@ import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService; @@ -19,7 +24,9 @@ import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
19 import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService; 24 import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
20 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; 25 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
21 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; 26 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
  27 +import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
22 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; 28 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
  29 +import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
23 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 30 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
24 import jakarta.annotation.Resource; 31 import jakarta.annotation.Resource;
25 import org.junit.jupiter.api.Test; 32 import org.junit.jupiter.api.Test;
@@ -30,6 +37,7 @@ import java.math.BigDecimal; @@ -30,6 +37,7 @@ import java.math.BigDecimal;
30 37
31 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 38 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
32 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; 39 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  40 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
33 import static org.junit.jupiter.api.Assertions.*; 41 import static org.junit.jupiter.api.Assertions.*;
34 import static org.mockito.Mockito.*; 42 import static org.mockito.Mockito.*;
35 43
@@ -69,11 +77,16 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -69,11 +77,16 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
69 private MesProRouteProcessService routeProcessService; 77 private MesProRouteProcessService routeProcessService;
70 @MockitoBean 78 @MockitoBean
71 private MesProTaskService taskService; 79 private MesProTaskService taskService;
  80 + @MockitoBean
  81 + private MesQcIndicatorResultService indicatorResultService;
  82 + @MockitoBean
  83 + private MesWmProductProduceLineService productProduceLineService;
72 84
73 @Test 85 @Test
74 public void testFinishIpqc_writeBack_feedback() { 86 public void testFinishIpqc_writeBack_feedback() {
75 // 准备数据:插入一条草稿状态的 IPQC 单,来源为报工单(PRO_FEEDBACK=304) 87 // 准备数据:插入一条草稿状态的 IPQC 单,来源为报工单(PRO_FEEDBACK=304)
76 Long sourceDocId = randomLongId(); 88 Long sourceDocId = randomLongId();
  89 + Long sourceLineId = randomLongId();
77 BigDecimal qualifiedQty = BigDecimal.valueOf(80); 90 BigDecimal qualifiedQty = BigDecimal.valueOf(80);
78 BigDecimal unqualifiedQty = BigDecimal.valueOf(20); 91 BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
79 BigDecimal laborScrapQty = BigDecimal.valueOf(5); 92 BigDecimal laborScrapQty = BigDecimal.valueOf(5);
@@ -85,7 +98,7 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -85,7 +98,7 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
85 o.setCheckResult(1); // 非空,满足 finishIpqc 的校验 98 o.setCheckResult(1); // 非空,满足 finishIpqc 的校验
86 o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); 99 o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
87 o.setSourceDocId(sourceDocId); 100 o.setSourceDocId(sourceDocId);
88 - o.setSourceLineId(null); // 报工场景下预留不使用 101 + o.setSourceLineId(sourceLineId);
89 o.setQualifiedQuantity(qualifiedQty); 102 o.setQualifiedQuantity(qualifiedQty);
90 o.setUnqualifiedQuantity(unqualifiedQty); 103 o.setUnqualifiedQuantity(unqualifiedQty);
91 o.setLaborScrapQuantity(laborScrapQty); 104 o.setLaborScrapQuantity(laborScrapQty);
@@ -101,10 +114,11 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -101,10 +114,11 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
101 MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId()); 114 MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
102 assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus()); 115 assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
103 116
104 - // 断言 2:调用了 feedbackService.completeFeedbackFromIpqc,传递正确参数 117 + // 断言 2:调用了 feedbackService,传递 sourceLineId
105 // 注意:数量经过 DB 存取后 scale 可能变化(例如 80 → 80.00),所以用 any() 匹配 118 // 注意:数量经过 DB 存取后 scale 可能变化(例如 80 → 80.00),所以用 any() 匹配
106 verify(feedbackService).updateProFeedbackWhenIpqcFinish( 119 verify(feedbackService).updateProFeedbackWhenIpqcFinish(
107 eq(sourceDocId), 120 eq(sourceDocId),
  121 + eq(sourceLineId),
108 any(BigDecimal.class), 122 any(BigDecimal.class),
109 any(BigDecimal.class), 123 any(BigDecimal.class),
110 any(BigDecimal.class), 124 any(BigDecimal.class),
@@ -116,12 +130,13 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -116,12 +130,13 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
116 public void testFinishIpqc_writeBack_feedback_withNullQuantities() { 130 public void testFinishIpqc_writeBack_feedback_withNullQuantities() {
117 // 准备数据:合格品/不合格品/废品数量为 null 的场景,应该 defaultIfNull 为 0 131 // 准备数据:合格品/不合格品/废品数量为 null 的场景,应该 defaultIfNull 为 0
118 Long sourceDocId = randomLongId(); 132 Long sourceDocId = randomLongId();
  133 + Long sourceLineId = randomLongId();
119 MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> { 134 MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
120 o.setStatus(MesQcStatusEnum.DRAFT.getStatus()); 135 o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
121 o.setCheckResult(1); 136 o.setCheckResult(1);
122 o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK); 137 o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
123 o.setSourceDocId(sourceDocId); 138 o.setSourceDocId(sourceDocId);
124 - o.setSourceLineId(null); 139 + o.setSourceLineId(sourceLineId);
125 o.setQualifiedQuantity(null); 140 o.setQualifiedQuantity(null);
126 o.setUnqualifiedQuantity(null); 141 o.setUnqualifiedQuantity(null);
127 o.setLaborScrapQuantity(null); 142 o.setLaborScrapQuantity(null);
@@ -136,6 +151,7 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -136,6 +151,7 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
136 // 断言:所有数量参数都是 BigDecimal.ZERO(defaultIfNull 处理) 151 // 断言:所有数量参数都是 BigDecimal.ZERO(defaultIfNull 处理)
137 verify(feedbackService).updateProFeedbackWhenIpqcFinish( 152 verify(feedbackService).updateProFeedbackWhenIpqcFinish(
138 eq(sourceDocId), 153 eq(sourceDocId),
  154 + eq(sourceLineId),
139 eq(BigDecimal.ZERO), 155 eq(BigDecimal.ZERO),
140 eq(BigDecimal.ZERO), 156 eq(BigDecimal.ZERO),
141 eq(BigDecimal.ZERO), 157 eq(BigDecimal.ZERO),
@@ -164,7 +180,7 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -164,7 +180,7 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
164 180
165 // 断言 2:不应该调用 feedbackService 181 // 断言 2:不应该调用 feedbackService
166 verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish( 182 verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
167 - anyLong(), any(), any(), any(), any(), any()); 183 + anyLong(), anyLong(), any(), any(), any(), any(), any());
168 } 184 }
169 185
170 @Test 186 @Test
@@ -308,6 +324,13 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -308,6 +324,13 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
308 workOrder.setProductId(productId); 324 workOrder.setProductId(productId);
309 when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder); 325 when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
310 when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder); 326 when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
  327 + // mock 任务
  328 + MesProTaskDO task = new MesProTaskDO();
  329 + task.setId(taskId);
  330 + task.setWorkOrderId(workOrderId);
  331 + task.setWorkstationId(workstationId);
  332 + task.setItemId(productId);
  333 + when(taskService.validateTaskNotFinished(taskId)).thenReturn(task);
311 // mock 模板匹配 334 // mock 模板匹配
312 MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO(); 335 MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
313 templateItem.setTemplateId(templateId); 336 templateItem.setTemplateId(templateId);
@@ -374,5 +397,200 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest { @@ -374,5 +397,200 @@ public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
374 assertNull(ipqc.getProcessId()); 397 assertNull(ipqc.getProcessId());
375 } 398 }
376 399
377 -} 400 + // ==================== sourceLineId 校验测试 ====================
  401 +
  402 + /**
  403 + * sourceDocType=PRO_FEEDBACK 时 sourceLineId 为空 → 创建失败
  404 + */
  405 + @Test
  406 + public void testCreateIpqc_sourceLineId_required() {
  407 + // 准备参数
  408 + Long workstationId = randomLongId();
  409 + Long workOrderId = randomLongId();
  410 + Long productId = randomLongId();
  411 + Long feedbackId = randomLongId();
  412 + Long templateId = randomLongId();
  413 +
  414 + MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
  415 + reqVO.setCode("IPQC-SL-001");
  416 + reqVO.setWorkstationId(workstationId);
  417 + reqVO.setWorkOrderId(workOrderId);
  418 + reqVO.setInspectorUserId(randomLongId());
  419 + reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  420 + reqVO.setSourceDocId(feedbackId);
  421 + reqVO.setSourceLineId(null); // 不传 sourceLineId
  422 +
  423 + // mock
  424 + MesMdWorkstationDO workstation = new MesMdWorkstationDO();
  425 + workstation.setId(workstationId);
  426 + workstation.setProcessId(null);
  427 + when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
  428 + MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
  429 + workOrder.setId(workOrderId);
  430 + workOrder.setProductId(productId);
  431 + when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
  432 + when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
  433 + MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
  434 + templateItem.setTemplateId(templateId);
  435 + when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
  436 + when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
378 437
  438 + // 调用,断言异常
  439 + ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
  440 + assertEquals(QC_IPQC_SOURCE_LINE_REQUIRED.getCode(), ex.getCode());
  441 + }
  442 +
  443 + /**
  444 + * sourceLineId 不属于该报工的产出行 → 创建失败
  445 + */
  446 + @Test
  447 + public void testCreateIpqc_sourceLineId_notBelong() {
  448 + // 准备参数
  449 + Long workstationId = randomLongId();
  450 + Long workOrderId = randomLongId();
  451 + Long productId = randomLongId();
  452 + Long feedbackId = randomLongId();
  453 + Long sourceLineId = randomLongId();
  454 + Long templateId = randomLongId();
  455 +
  456 + MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
  457 + reqVO.setCode("IPQC-SL-002");
  458 + reqVO.setWorkstationId(workstationId);
  459 + reqVO.setWorkOrderId(workOrderId);
  460 + reqVO.setInspectorUserId(randomLongId());
  461 + reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  462 + reqVO.setSourceDocId(feedbackId);
  463 + reqVO.setSourceLineId(sourceLineId);
  464 +
  465 + // mock
  466 + MesMdWorkstationDO workstation = new MesMdWorkstationDO();
  467 + workstation.setId(workstationId);
  468 + workstation.setProcessId(null);
  469 + when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
  470 + MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
  471 + workOrder.setId(workOrderId);
  472 + workOrder.setProductId(productId);
  473 + when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
  474 + when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
  475 + MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
  476 + templateItem.setTemplateId(templateId);
  477 + when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
  478 + when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
  479 + // mock:产出行存在但 feedbackId 不匹配
  480 + MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
  481 + line.setId(sourceLineId);
  482 + line.setFeedbackId(randomLongId()); // 不同的 feedbackId
  483 + line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus());
  484 + when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
  485 +
  486 + // 调用,断言异常
  487 + ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
  488 + assertEquals(QC_IPQC_SOURCE_LINE_NOT_BELONG.getCode(), ex.getCode());
  489 + }
  490 +
  491 + /**
  492 + * sourceLineId 对应的产出行不是待检验状态 → 创建失败
  493 + */
  494 + @Test
  495 + public void testCreateIpqc_sourceLineId_notPending() {
  496 + // 准备参数
  497 + Long workstationId = randomLongId();
  498 + Long workOrderId = randomLongId();
  499 + Long productId = randomLongId();
  500 + Long feedbackId = randomLongId();
  501 + Long sourceLineId = randomLongId();
  502 + Long templateId = randomLongId();
  503 +
  504 + MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
  505 + reqVO.setCode("IPQC-SL-003");
  506 + reqVO.setWorkstationId(workstationId);
  507 + reqVO.setWorkOrderId(workOrderId);
  508 + reqVO.setInspectorUserId(randomLongId());
  509 + reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  510 + reqVO.setSourceDocId(feedbackId);
  511 + reqVO.setSourceLineId(sourceLineId);
  512 +
  513 + // mock
  514 + MesMdWorkstationDO workstation = new MesMdWorkstationDO();
  515 + workstation.setId(workstationId);
  516 + workstation.setProcessId(null);
  517 + when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
  518 + MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
  519 + workOrder.setId(workOrderId);
  520 + workOrder.setProductId(productId);
  521 + when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
  522 + when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
  523 + MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
  524 + templateItem.setTemplateId(templateId);
  525 + when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
  526 + when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
  527 + // mock:产出行存在且归属正确,但不是 PENDING 状态
  528 + MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
  529 + line.setId(sourceLineId);
  530 + line.setFeedbackId(feedbackId);
  531 + line.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); // 已合格,非待检
  532 + when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
  533 +
  534 + // 调用,断言异常
  535 + ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
  536 + assertEquals(QC_IPQC_SOURCE_LINE_NOT_PENDING.getCode(), ex.getCode());
  537 + }
  538 +
  539 + /**
  540 + * sourceLineId 校验全部通过 → 创建成功
  541 + */
  542 + @Test
  543 + public void testCreateIpqc_sourceLineId_success() {
  544 + // 准备参数
  545 + Long workstationId = randomLongId();
  546 + Long workOrderId = randomLongId();
  547 + Long productId = randomLongId();
  548 + Long feedbackId = randomLongId();
  549 + Long sourceLineId = randomLongId();
  550 + Long templateId = randomLongId();
  551 +
  552 + MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
  553 + reqVO.setCode("IPQC-SL-004");
  554 + reqVO.setWorkstationId(workstationId);
  555 + reqVO.setWorkOrderId(workOrderId);
  556 + reqVO.setInspectorUserId(randomLongId());
  557 + reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  558 + reqVO.setSourceDocId(feedbackId);
  559 + reqVO.setSourceLineId(sourceLineId);
  560 +
  561 + // mock
  562 + MesMdWorkstationDO workstation = new MesMdWorkstationDO();
  563 + workstation.setId(workstationId);
  564 + workstation.setProcessId(null);
  565 + when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
  566 + MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
  567 + workOrder.setId(workOrderId);
  568 + workOrder.setProductId(productId);
  569 + when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
  570 + when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
  571 + MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
  572 + templateItem.setTemplateId(templateId);
  573 + when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
  574 + MesProFeedbackDO feedback = new MesProFeedbackDO();
  575 + feedback.setId(feedbackId);
  576 + feedback.setCode("FB-001");
  577 + when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(feedback);
  578 + // mock:产出行存在、归属正确、PENDING 状态
  579 + MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
  580 + line.setId(sourceLineId);
  581 + line.setFeedbackId(feedbackId);
  582 + line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus());
  583 + when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
  584 +
  585 + // 调用
  586 + Long ipqcId = ipqcService.createIpqc(reqVO);
  587 +
  588 + // 断言:创建成功,sourceDocCode 回写
  589 + MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
  590 + assertNotNull(ipqc);
  591 + assertEquals(feedbackId, ipqc.getSourceDocId());
  592 + assertEquals(sourceLineId, ipqc.getSourceLineId());
  593 + assertEquals("FB-001", ipqc.getSourceDocCode());
  594 + }
  595 +
  596 +}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImplTest.java
1 package cn.iocoder.yudao.module.mes.service.wm.productproduce; 1 package cn.iocoder.yudao.module.mes.service.wm.productproduce;
2 2
3 import cn.hutool.core.collection.ListUtil; 3 import cn.hutool.core.collection.ListUtil;
  4 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
4 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
5 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO; 6 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
6 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO; 7 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
@@ -27,8 +28,8 @@ import java.math.BigDecimal; @@ -27,8 +28,8 @@ import java.math.BigDecimal;
27 28
28 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 29 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
29 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; 30 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
30 -import static org.junit.jupiter.api.Assertions.assertEquals;  
31 -import static org.junit.jupiter.api.Assertions.assertThrows; 31 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
  32 +import static org.junit.jupiter.api.Assertions.*;
32 import static org.mockito.Mockito.*; 33 import static org.mockito.Mockito.*;
33 34
34 /** 35 /**
@@ -66,6 +67,7 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -66,6 +67,7 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
66 public void testSplitPendingAndFinishProduce_withUnqualified() { 67 public void testSplitPendingAndFinishProduce_withUnqualified() {
67 // 准备数据:插入一个 PREPARE 状态的产出单 68 // 准备数据:插入一个 PREPARE 状态的产出单
68 Long feedbackId = randomLongId(); 69 Long feedbackId = randomLongId();
  70 + Long sourceLineId = 100L;
69 Long itemId = randomLongId(); 71 Long itemId = randomLongId();
70 Long batchId = randomLongId(); 72 Long batchId = randomLongId();
71 String batchCode = "BATCH-001"; 73 String batchCode = "BATCH-001";
@@ -85,19 +87,17 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -85,19 +87,17 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
85 when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION)).thenReturn(loc); 87 when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION)).thenReturn(loc);
86 when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA)).thenReturn(area); 88 when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA)).thenReturn(area);
87 89
88 - // mock: 返回一条 PENDING 的产出行 90 + // mock: 通过 sourceLineId 直接定位 PENDING 产出行
89 MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder() 91 MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder()
90 - .id(100L).produceId(produce.getId()).feedbackId(feedbackId) 92 + .id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId)
91 .itemId(itemId).quantity(BigDecimal.valueOf(100)) 93 .itemId(itemId).quantity(BigDecimal.valueOf(100))
92 .batchId(batchId).batchCode(batchCode) 94 .batchId(batchId).batchCode(batchCode)
93 .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()) 95 .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
94 .build(); 96 .build();
95 - when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))  
96 - .thenReturn(ListUtil.of(pendingLine)); 97 + when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
  98 + .thenReturn(pendingLine);
97 99
98 - // mock: finishProductProduce 内部会再查一次行和明细(用于创建库存事务)  
99 - // 由于 finishProductProduce 在拆分行后调用,此时行已经变了,但由于 line/detail 是 mock 的,  
100 - // 我们需要 mock 已拆分后的行给 finishProductProduce 100 + // mock: finishProductProduce 内部查行和明细(用于创建库存事务)
101 MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder() 101 MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
102 .id(100L).produceId(produce.getId()).itemId(itemId) 102 .id(100L).produceId(produce.getId()).itemId(itemId)
103 .quantity(BigDecimal.valueOf(80)) 103 .quantity(BigDecimal.valueOf(80))
@@ -108,10 +108,8 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -108,10 +108,8 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
108 .quantity(BigDecimal.valueOf(20)) 108 .quantity(BigDecimal.valueOf(20))
109 .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus()) 109 .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
110 .build(); 110 .build();
111 - // finishProductProduce 内部第二次调用 getProductProduceLineListByProduceId  
112 when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId())) 111 when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
113 - .thenReturn(ListUtil.of(pendingLine)) // 第 1 次:splitPendingAndFinishProduce 查待检行  
114 - .thenReturn(ListUtil.of(qualifiedLine, unqualifiedLine)); // 第 2 次:finishProductProduce 查所有行 112 + .thenReturn(ListUtil.of(qualifiedLine, unqualifiedLine));
115 113
116 // mock: finishProductProduce 中按行查明细 114 // mock: finishProductProduce 中按行查明细
117 MesWmProductProduceDetailDO qualifiedDetail = MesWmProductProduceDetailDO.builder() 115 MesWmProductProduceDetailDO qualifiedDetail = MesWmProductProduceDetailDO.builder()
@@ -124,7 +122,8 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -124,7 +122,8 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
124 .thenReturn(ListUtil.of(unqualifiedDetail)); 122 .thenReturn(ListUtil.of(unqualifiedDetail));
125 123
126 // 调用 124 // 调用
127 - productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.valueOf(80), BigDecimal.valueOf(20)); 125 + productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
  126 + BigDecimal.valueOf(80), BigDecimal.valueOf(20));
128 127
129 // 断言 1:不合格品行 - 新建了一行 128 // 断言 1:不合格品行 - 新建了一行
130 ArgumentCaptor<MesWmProductProduceLineDO> lineCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class); 129 ArgumentCaptor<MesWmProductProduceLineDO> lineCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
@@ -156,6 +155,7 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -156,6 +155,7 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
156 public void testSplitPendingAndFinishProduce_allQualified() { 155 public void testSplitPendingAndFinishProduce_allQualified() {
157 // 准备数据:全部合格品场景 156 // 准备数据:全部合格品场景
158 Long feedbackId = randomLongId(); 157 Long feedbackId = randomLongId();
  158 + Long sourceLineId = 200L;
159 Long itemId = randomLongId(); 159 Long itemId = randomLongId();
160 160
161 MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> { 161 MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
@@ -173,30 +173,31 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -173,30 +173,31 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
173 when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc); 173 when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc);
174 when(areaService.getWarehouseAreaByCode(any())).thenReturn(area); 174 when(areaService.getWarehouseAreaByCode(any())).thenReturn(area);
175 175
  176 + // mock: 通过 sourceLineId 直接定位 PENDING 产出行
176 MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder() 177 MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder()
177 - .id(200L).produceId(produce.getId()).feedbackId(feedbackId) 178 + .id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId)
178 .itemId(itemId).quantity(BigDecimal.valueOf(50)) 179 .itemId(itemId).quantity(BigDecimal.valueOf(50))
179 .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()) 180 .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
180 .build(); 181 .build();
  182 + when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
  183 + .thenReturn(pendingLine);
181 184
182 - // 第 1 次查行(拆分阶段)返回 PENDING 行;第 2 次查行(finishProductProduce)返回已更新的合格行 185 + // mock: finishProductProduce 查行和明细
183 MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder() 186 MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
184 .id(200L).produceId(produce.getId()).itemId(itemId) 187 .id(200L).produceId(produce.getId()).itemId(itemId)
185 .quantity(BigDecimal.valueOf(50)) 188 .quantity(BigDecimal.valueOf(50))
186 .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus()) 189 .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
187 .build(); 190 .build();
188 when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId())) 191 when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
189 - .thenReturn(ListUtil.of(pendingLine))  
190 .thenReturn(ListUtil.of(qualifiedLine)); 192 .thenReturn(ListUtil.of(qualifiedLine));
191 -  
192 - // mock: finishProductProduce 中按行查明细  
193 MesWmProductProduceDetailDO detail = MesWmProductProduceDetailDO.builder() 193 MesWmProductProduceDetailDO detail = MesWmProductProduceDetailDO.builder()
194 .lineId(200L).quantity(BigDecimal.valueOf(50)).build(); 194 .lineId(200L).quantity(BigDecimal.valueOf(50)).build();
195 when(productProduceDetailService.getProductProduceDetailListByLineId(200L)) 195 when(productProduceDetailService.getProductProduceDetailListByLineId(200L))
196 .thenReturn(ListUtil.of(detail)); 196 .thenReturn(ListUtil.of(detail));
197 197
198 // 调用:不合格品数量 = 0 198 // 调用:不合格品数量 = 0
199 - productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.valueOf(50), BigDecimal.ZERO); 199 + productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
  200 + BigDecimal.valueOf(50), BigDecimal.ZERO);
200 201
201 // 断言 1:没有新建行(不合格品数量为 0) 202 // 断言 1:没有新建行(不合格品数量为 0)
202 verify(productProduceLineService, never()).createProductProduceLine(any()); 203 verify(productProduceLineService, never()).createProductProduceLine(any());
@@ -218,14 +219,18 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -218,14 +219,18 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
218 public void testSplitPendingAndFinishProduce_produceNotExists() { 219 public void testSplitPendingAndFinishProduce_produceNotExists() {
219 // 调用不存在的 feedbackId,应该抛异常 220 // 调用不存在的 feedbackId,应该抛异常
220 Long feedbackId = randomLongId(); 221 Long feedbackId = randomLongId();
221 - assertThrows(Exception.class, () ->  
222 - productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.TEN, BigDecimal.ZERO)); 222 + Long sourceLineId = randomLongId();
  223 + ServiceException ex = assertThrows(ServiceException.class, () ->
  224 + productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
  225 + BigDecimal.TEN, BigDecimal.ZERO));
  226 + assertEquals(WM_PRODUCT_PRODUCE_NOT_EXISTS.getCode(), ex.getCode());
223 } 227 }
224 228
225 @Test 229 @Test
226 - public void testSplitPendingAndFinishProduce_noPendingLine() {  
227 - // 准备数据:产出单存在,但没有 PENDING 行 230 + public void testSplitPendingAndFinishProduce_lineNotExists() {
  231 + // 准备数据:产出单存在,但 sourceLineId 对应的行不存在
228 Long feedbackId = randomLongId(); 232 Long feedbackId = randomLongId();
  233 + Long sourceLineId = randomLongId();
229 MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> { 234 MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
230 o.setFeedbackId(feedbackId); 235 o.setFeedbackId(feedbackId);
231 o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus()); 236 o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
@@ -240,17 +245,15 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @@ -240,17 +245,15 @@ public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
240 when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc); 245 when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc);
241 when(areaService.getWarehouseAreaByCode(any())).thenReturn(area); 246 when(areaService.getWarehouseAreaByCode(any())).thenReturn(area);
242 247
243 - // mock: 返回一条 PASS 状态的行(不是 PENDING)  
244 - MesWmProductProduceLineDO passLine = MesWmProductProduceLineDO.builder()  
245 - .id(300L).produceId(produce.getId())  
246 - .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())  
247 - .build();  
248 - when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))  
249 - .thenReturn(ListUtil.of(passLine)); 248 + // mock: validateProductProduceLineExists 抛异常
  249 + when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
  250 + .thenThrow(new ServiceException(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS));
250 251
251 - // 调用,应该抛异常(找不到 PENDING 行)  
252 - assertThrows(Exception.class, () ->  
253 - productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.TEN, BigDecimal.ZERO)); 252 + // 调用,应该抛异常
  253 + ServiceException ex = assertThrows(ServiceException.class, () ->
  254 + productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
  255 + BigDecimal.TEN, BigDecimal.ZERO));
  256 + assertEquals(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS.getCode(), ex.getCode());
254 } 257 }
255 258
256 } 259 }