Commit 91e7b43b1d32617684da1b4a1750957e0f01e5d2

Authored by YunaiV
1 parent 828c878f

✨ feat(mes): 添加检测工具字段,优化出货检验单数据结构

新增检测工具字段以支持更全面的检验信息,同时优化相关数据结构,提升系统的可用性和灵活性。
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcPageReqVO.java
@@ -27,10 +27,4 @@ public class MesQcOqcPageReqVO extends PageParam { @@ -27,10 +27,4 @@ public class MesQcOqcPageReqVO extends PageParam {
27 @Schema(description = "检测结果", example = "1") 27 @Schema(description = "检测结果", example = "1")
28 private Integer checkResult; 28 private Integer checkResult;
29 29
30 - @Schema(description = "状态", example = "0")  
31 - private Integer status;  
32 -  
33 - @Schema(description = "检测人员用户 ID", example = "1")  
34 - private Long inspectorUserId;  
35 -  
36 } 30 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/line/MesQcOqcLineRespVO.java
@@ -30,6 +30,9 @@ public class MesQcOqcLineRespVO { @@ -30,6 +30,9 @@ public class MesQcOqcLineRespVO {
30 @Schema(description = "检测指标类型", example = "QUANTITATIVE") 30 @Schema(description = "检测指标类型", example = "QUANTITATIVE")
31 private String indicatorType; 31 private String indicatorType;
32 32
  33 + @Schema(description = "检测工具", example = "卡尺")
  34 + private String tool;
  35 +
33 @Schema(description = "检测方法", example = "目测") 36 @Schema(description = "检测方法", example = "目测")
34 private String checkMethod; 37 private String checkMethod;
35 38
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/oqc/MesQcOqcDO.java
@@ -99,6 +99,14 @@ public class MesQcOqcDO extends BaseDO { @@ -99,6 +99,14 @@ public class MesQcOqcDO extends BaseDO {
99 // ========== 数量 ========== 99 // ========== 数量 ==========
100 100
101 /** 101 /**
  102 + * 最低检测数
  103 + */
  104 + private Integer minCheckQuantity;
  105 + /**
  106 + * 最大不合格数
  107 + */
  108 + private Integer maxUnqualifiedQuantity;
  109 + /**
102 * 本次出货数量 110 * 本次出货数量
103 */ 111 */
104 private BigDecimal outQuantity; 112 private BigDecimal outQuantity;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/oqc/MesQcOqcMapper.java
@@ -22,8 +22,6 @@ public interface MesQcOqcMapper extends BaseMapperX<MesQcOqcDO> { @@ -22,8 +22,6 @@ public interface MesQcOqcMapper extends BaseMapperX<MesQcOqcDO> {
22 .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode()) 22 .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
23 .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId()) 23 .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
24 .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult()) 24 .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
25 - .eqIfPresent(MesQcOqcDO::getStatus, reqVO.getStatus())  
26 - .eqIfPresent(MesQcOqcDO::getInspectorUserId, reqVO.getInspectorUserId())  
27 .orderByDesc(MesQcOqcDO::getId)); 25 .orderByDesc(MesQcOqcDO::getId));
28 } 26 }
29 27
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -75,17 +75,18 @@ public class MesQcOqcServiceImpl implements MesQcOqcService { @@ -75,17 +75,18 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
75 public Long createOqc(MesQcOqcSaveReqVO createReqVO) { 75 public Long createOqc(MesQcOqcSaveReqVO createReqVO) {
76 // 1.1 校验编号唯一 76 // 1.1 校验编号唯一
77 validateOqcCodeUnique(null, createReqVO.getCode()); 77 validateOqcCodeUnique(null, createReqVO.getCode());
78 - // 1.2 校验客户、物料、检测人员存在  
79 - clientService.validateClientExists(createReqVO.getClientId()); 78 + // 1.2 校验物料、检测人员存在
80 itemService.validateItemExists(createReqVO.getItemId()); 79 itemService.validateItemExists(createReqVO.getItemId());
81 adminUserApi.validateUser(createReqVO.getInspectorUserId()); 80 adminUserApi.validateUser(createReqVO.getInspectorUserId());
82 - // 1.3 根据产品 + 检验类型自动匹配模板 81 + // 1.3 校验来源单据参数,并验证数据存在
  82 + String sourceDocCode = validateAndGetSourceDocCode(
  83 + createReqVO.getSourceDocType(), createReqVO.getSourceLineId());
  84 + // 1.4 校验客户存在
  85 + clientService.validateClientExists(createReqVO.getClientId());
  86 + // 1.5 根据产品 + 检验类型自动匹配模板
83 MesQcTemplateItemDO templateItem = templateDetailService.getRequiredTemplateByItemIdAndType( 87 MesQcTemplateItemDO templateItem = templateDetailService.getRequiredTemplateByItemIdAndType(
84 createReqVO.getItemId(), MesQcTypeEnum.OQC.getType()); 88 createReqVO.getItemId(), MesQcTypeEnum.OQC.getType());
85 Long templateId = templateItem.getTemplateId(); 89 Long templateId = templateItem.getTemplateId();
86 - // 1.4 获取来源单据编号  
87 - String sourceDocCode = validateAndGetSourceDocCode(  
88 - createReqVO.getSourceDocType(), createReqVO.getSourceLineId());  
89 90
90 // 2. 插入主表 91 // 2. 插入主表
91 MesQcOqcDO oqc = BeanUtils.toBean(createReqVO, MesQcOqcDO.class) 92 MesQcOqcDO oqc = BeanUtils.toBean(createReqVO, MesQcOqcDO.class)
@@ -137,7 +138,9 @@ public class MesQcOqcServiceImpl implements MesQcOqcService { @@ -137,7 +138,9 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
137 /** 138 /**
138 * 回写来源单据(OQC 完成后) 139 * 回写来源单据(OQC 完成后)
139 * 140 *
140 - * <p>当来源为销售出库单时,回写行的 oqcId 和 qualityStatus 141 + * <p>当来源为销售出库单时,调用
  142 + * {@link MesWmProductSalesLineService#updateProductSalesLineWhenOqcFinish}
  143 + * 回写行的 oqcId、qualityStatus,并由该方法内部联动检查出库单整体状态
141 * 144 *
142 * @param oqc 出货检验单 145 * @param oqc 出货检验单
143 */ 146 */
@@ -151,8 +154,8 @@ public class MesQcOqcServiceImpl implements MesQcOqcService { @@ -151,8 +154,8 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
151 } 154 }
152 155
153 if (Objects.equals(oqc.getSourceDocType(), MesBizTypeConstants.WM_PRODUCT_SALES)) { 156 if (Objects.equals(oqc.getSourceDocType(), MesBizTypeConstants.WM_PRODUCT_SALES)) {
154 - // 回写销售出库单行的 oqcId + qualityStatus  
155 - productSalesLineService.updateProductSalesLineWhenOqcFinish(oqc.getSourceLineId(), oqc.getId(), oqc.getCheckResult()); 157 + productSalesLineService.updateProductSalesLineWhenOqcFinish(
  158 + oqc.getSourceLineId(), oqc.getId(), oqc.getCheckResult());
156 } else { 159 } else {
157 throw new IllegalArgumentException( 160 throw new IllegalArgumentException(
158 "OQC 单[" + oqc.getId() + "] sourceDocType=" + oqc.getSourceDocType() + " 无法识别,无法回写来源单据"); 161 "OQC 单[" + oqc.getId() + "] sourceDocType=" + oqc.getSourceDocType() + " 无法识别,无法回写来源单据");
@@ -203,6 +206,13 @@ public class MesQcOqcServiceImpl implements MesQcOqcService { @@ -203,6 +206,13 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
203 } 206 }
204 } 207 }
205 208
  209 + /**
  210 + * 校验来源单据参数,并验证数据存在
  211 + *
  212 + * @param sourceDocType 来源单据类型
  213 + * @param sourceLineId 来源单据行 ID
  214 + * @return 来源单据编号
  215 + */
206 private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceLineId) { 216 private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceLineId) {
207 if (sourceDocType == null || sourceLineId == null) { 217 if (sourceDocType == null || sourceLineId == null) {
208 return null; 218 return null;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java
1 package cn.iocoder.yudao.module.mes.service.wm.productsales; 1 package cn.iocoder.yudao.module.mes.service.wm.productsales;
2 2
  3 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLinePageReqVO; 5 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLinePageReqVO;
5 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLineSaveReqVO; 6 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLineSaveReqVO;
@@ -9,6 +10,8 @@ import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum; @@ -9,6 +10,8 @@ import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
9 import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; 10 import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
10 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; 11 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
11 import jakarta.annotation.Resource; 12 import jakarta.annotation.Resource;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.context.annotation.Lazy;
12 import org.springframework.stereotype.Service; 15 import org.springframework.stereotype.Service;
13 import org.springframework.transaction.annotation.Transactional; 16 import org.springframework.transaction.annotation.Transactional;
14 import org.springframework.validation.annotation.Validated; 17 import org.springframework.validation.annotation.Validated;
@@ -26,6 +29,7 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_SA @@ -26,6 +29,7 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_SA
26 */ 29 */
27 @Service 30 @Service
28 @Validated 31 @Validated
  32 +@Slf4j
29 public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineService { 33 public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineService {
30 34
31 @Resource 35 @Resource
@@ -34,6 +38,9 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe @@ -34,6 +38,9 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe
34 private MesWmProductSalesDetailService productSalesDetailService; 38 private MesWmProductSalesDetailService productSalesDetailService;
35 @Resource 39 @Resource
36 private MesMdItemService itemService; 40 private MesMdItemService itemService;
  41 + @Resource
  42 + @Lazy
  43 + private MesWmProductSalesService productSalesService;
37 44
38 @Override 45 @Override
39 public Long createProductSalesLine(MesWmProductSalesLineSaveReqVO createReqVO) { 46 public Long createProductSalesLine(MesWmProductSalesLineSaveReqVO createReqVO) {
@@ -100,11 +107,29 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe @@ -100,11 +107,29 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe
100 } 107 }
101 108
102 @Override 109 @Override
  110 + @Transactional(rollbackFor = Exception.class)
103 public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) { 111 public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) {
  112 + // 1. 更新行的 oqcId + qualityStatus
104 Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.PASS.getType()) 113 Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.PASS.getType())
105 ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus(); 114 ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus();
106 productSalesLineMapper.updateById(new MesWmProductSalesLineDO() 115 productSalesLineMapper.updateById(new MesWmProductSalesLineDO()
107 .setId(id).setOqcId(oqcId).setQualityStatus(qualityStatus)); 116 .setId(id).setOqcId(oqcId).setQualityStatus(qualityStatus));
  117 +
  118 + // 2. 检查同一出库单下所有需要 OQC 检验的行的质检状态,联动更新出库单整体状态
  119 + MesWmProductSalesLineDO currentLine = productSalesLineMapper.selectById(id);
  120 + if (currentLine == null || currentLine.getSalesId() == null) {
  121 + return;
  122 + }
  123 + Long salesId = currentLine.getSalesId();
  124 + List<MesWmProductSalesLineDO> allLines = productSalesLineMapper.selectListBySalesId(salesId);
  125 + boolean hasNg = CollectionUtils.anyMatch(allLines,
  126 + line -> Boolean.TRUE.equals(line.getOqcCheck())
  127 + && Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus()));
  128 + if (hasNg) {
  129 + log.info("[updateProductSalesLineWhenOqcFinish][销售出库单({}) 存在质检不合格行,取消出库单]", salesId);
  130 + productSalesService.cancelProductSales(salesId);
  131 + }
108 } 132 }
109 133
110 } 134 }
  135 +
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
@@ -5,10 +5,12 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO; @@ -5,10 +5,12 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
5 import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper; 5 import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
6 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; 6 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
7 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; 7 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
  8 +import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
8 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; 9 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
9 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; 10 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
10 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateDetailService; 11 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateDetailService;
11 import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService; 12 import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService;
  13 +import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
12 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 14 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
13 import jakarta.annotation.Resource; 15 import jakarta.annotation.Resource;
14 import org.junit.jupiter.api.Test; 16 import org.junit.jupiter.api.Test;
@@ -18,11 +20,10 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -18,11 +20,10 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
18 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; 20 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
19 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 21 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
20 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; 22 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
21 -import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.QC_OQC_NOT_EXISTS;  
22 -import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.QC_OQC_NOT_PREPARE;  
23 -import static org.junit.jupiter.api.Assertions.assertEquals; 23 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
  24 +import static org.junit.jupiter.api.Assertions.*;
24 import static org.mockito.ArgumentMatchers.eq; 25 import static org.mockito.ArgumentMatchers.eq;
25 -import static org.mockito.Mockito.verify; 26 +import static org.mockito.Mockito.*;
26 27
27 /** 28 /**
28 * {@link MesQcOqcServiceImpl} 的单元测试类 29 * {@link MesQcOqcServiceImpl} 的单元测试类
@@ -41,8 +42,12 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest { @@ -41,8 +42,12 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
41 @MockitoBean 42 @MockitoBean
42 private MesWmProductSalesLineService productSalesLineService; 43 private MesWmProductSalesLineService productSalesLineService;
43 @MockitoBean 44 @MockitoBean
  45 + private MesWmProductSalesService productSalesService;
  46 + @MockitoBean
44 private MesQcOqcLineService oqcLineService; 47 private MesQcOqcLineService oqcLineService;
45 @MockitoBean 48 @MockitoBean
  49 + private MesMdClientService clientService;
  50 + @MockitoBean
46 private MesMdItemService itemService; 51 private MesMdItemService itemService;
47 @MockitoBean 52 @MockitoBean
48 private MesQcTemplateDetailService templateDetailService; 53 private MesQcTemplateDetailService templateDetailService;
@@ -52,12 +57,10 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest { @@ -52,12 +57,10 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
52 private AdminUserApi adminUserApi; 57 private AdminUserApi adminUserApi;
53 58
54 @Test 59 @Test
55 - public void testFinishOqc_success() {  
56 - // 准备参数 60 + public void testFinishOqc_writeBack_productSales() {
  61 + // 准备数据:插入一条草稿状态的 OQC 单,来源为销售出库单
57 Long sourceLineId = randomLongId(); 62 Long sourceLineId = randomLongId();
58 Integer checkResult = 1; 63 Integer checkResult = 1;
59 -  
60 - // mock 数据  
61 MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> { 64 MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
62 o.setStatus(MesQcStatusEnum.DRAFT.getStatus()); 65 o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
63 o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES); 66 o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
@@ -69,12 +72,64 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest { @@ -69,12 +72,64 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
69 // 调用 72 // 调用
70 oqcService.finishOqc(oqc.getId()); 73 oqcService.finishOqc(oqc.getId());
71 74
72 - // 断言:验证状态更新 75 + // 断言:验证状态更新为已完成
73 MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId()); 76 MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
74 assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus()); 77 assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
  78 + // 断言:验证回写销售出库单行
  79 + verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(
  80 + eq(sourceLineId), eq(oqc.getId()), eq(checkResult));
  81 + }
75 82
76 - // 断言:验证回写调用  
77 - verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(eq(sourceLineId), eq(oqc.getId()), eq(checkResult)); 83 + @Test
  84 + public void testFinishOqc_writeBack_noSourceDoc() {
  85 + // 准备数据:sourceDocType 为空,不需要回写
  86 + MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
  87 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  88 + o.setCheckResult(1);
  89 + o.setSourceDocType(null);
  90 + o.setSourceLineId(null);
  91 + });
  92 + oqcMapper.insert(oqc);
  93 +
  94 + // 调用
  95 + oqcService.finishOqc(oqc.getId());
  96 +
  97 + // 断言:验证状态更新为已完成
  98 + MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
  99 + assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
  100 + // 断言:不应该调用任何回写
  101 + verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(
  102 + anyLong(), anyLong(), anyInt());
  103 + }
  104 +
  105 + @Test
  106 + public void testFinishOqc_writeBack_sourceDocTypeDirtyData() {
  107 + // 准备数据:sourceDocType 非空但 sourceLineId 为空,应该抛异常
  108 + MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
  109 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  110 + o.setCheckResult(1);
  111 + o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
  112 + o.setSourceLineId(null); // 脏数据
  113 + });
  114 + oqcMapper.insert(oqc);
  115 +
  116 + // 调用,并断言异常
  117 + assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
  118 + }
  119 +
  120 + @Test
  121 + public void testFinishOqc_writeBack_unknownSourceDocType() {
  122 + // 准备数据:sourceDocType 无法识别
  123 + MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
  124 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  125 + o.setCheckResult(1);
  126 + o.setSourceDocType(99999); // 无法识别的类型
  127 + o.setSourceLineId(randomLongId());
  128 + });
  129 + oqcMapper.insert(oqc);
  130 +
  131 + // 调用,并断言异常
  132 + assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
78 } 133 }
79 134
80 @Test 135 @Test
@@ -88,8 +143,7 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest { @@ -88,8 +143,7 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
88 143
89 @Test 144 @Test
90 public void testFinishOqc_statusNotDraft() { 145 public void testFinishOqc_statusNotDraft() {
91 - // 准备参数  
92 - // mock 数据:状态为已完成 146 + // 准备参数:状态为已完成
93 MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> { 147 MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
94 o.setStatus(MesQcStatusEnum.FINISHED.getStatus()); 148 o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
95 }); 149 });
@@ -99,4 +153,48 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest { @@ -99,4 +153,48 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
99 assertServiceException(() -> oqcService.finishOqc(oqc.getId()), QC_OQC_NOT_PREPARE); 153 assertServiceException(() -> oqcService.finishOqc(oqc.getId()), QC_OQC_NOT_PREPARE);
100 } 154 }
101 155
  156 + @Test
  157 + public void testFinishOqc_checkResultEmpty() {
  158 + // 准备数据:checkResult 为空,应该报错
  159 + MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
  160 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  161 + o.setCheckResult(null); // 检测结论为空
  162 + o.setSourceDocType(null);
  163 + });
  164 + oqcMapper.insert(oqc);
  165 +
  166 + // 调用,应该抛异常(检测结论必填)
  167 + assertServiceException(() -> oqcService.finishOqc(oqc.getId()), QC_OQC_CHECK_RESULT_EMPTY);
  168 + }
  169 +
  170 + @Test
  171 + public void testDeleteOqc_success() {
  172 + // 准备数据:草稿状态
  173 + MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
  174 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  175 + });
  176 + oqcMapper.insert(oqc);
  177 +
  178 + // 调用
  179 + oqcService.deleteOqc(oqc.getId());
  180 +
  181 + // 断言:已删除
  182 + assertNull(oqcMapper.selectById(oqc.getId()));
  183 + }
  184 +
  185 + @Test
  186 + public void testDeleteOqc_notExists() {
  187 + assertServiceException(() -> oqcService.deleteOqc(randomLongId()), QC_OQC_NOT_EXISTS);
  188 + }
  189 +
  190 + @Test
  191 + public void testDeleteOqc_statusNotDraft() {
  192 + MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
  193 + o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
  194 + });
  195 + oqcMapper.insert(oqc);
  196 +
  197 + assertServiceException(() -> oqcService.deleteOqc(oqc.getId()), QC_OQC_NOT_PREPARE);
  198 + }
  199 +
102 } 200 }
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImplTest.java
@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
15 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 15 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; 16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
17 import static org.junit.jupiter.api.Assertions.assertEquals; 17 import static org.junit.jupiter.api.Assertions.assertEquals;
  18 +import static org.mockito.Mockito.*;
18 19
19 /** 20 /**
20 * {@link MesWmProductSalesLineServiceImpl} 的单元测试类 21 * {@link MesWmProductSalesLineServiceImpl} 的单元测试类
@@ -46,13 +47,16 @@ public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest { @@ -46,13 +47,16 @@ public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest {
46 Integer checkResult = MesQcCheckResultEnum.PASS.getType(); 47 Integer checkResult = MesQcCheckResultEnum.PASS.getType();
47 48
48 // mock 数据 49 // mock 数据
49 - MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class); 50 + MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class, o -> {
  51 + o.setOqcCheck(true); // 需要 OQC 检验
  52 + o.setQualityStatus(null); // 初始为空
  53 + });
50 productSalesLineMapper.insert(lineDO); 54 productSalesLineMapper.insert(lineDO);
51 55
52 // 调用 56 // 调用
53 productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult); 57 productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult);
54 58
55 - // 断言:验证 mapper 更新参数正确 59 + // 断言:验证行字段更新
56 MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId()); 60 MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId());
57 assertEquals(oqcId, updateLine.getOqcId()); 61 assertEquals(oqcId, updateLine.getOqcId());
58 assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateLine.getQualityStatus()); 62 assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateLine.getQualityStatus());
@@ -65,16 +69,97 @@ public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest { @@ -65,16 +69,97 @@ public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest {
65 Integer checkResult = MesQcCheckResultEnum.FAIL.getType(); 69 Integer checkResult = MesQcCheckResultEnum.FAIL.getType();
66 70
67 // mock 数据 71 // mock 数据
68 - MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class); 72 + MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class, o -> {
  73 + o.setOqcCheck(true);
  74 + o.setQualityStatus(null);
  75 + });
69 productSalesLineMapper.insert(lineDO); 76 productSalesLineMapper.insert(lineDO);
70 77
71 // 调用 78 // 调用
72 productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult); 79 productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult);
73 80
74 - // 断言:验证 mapper 更新参数正确 81 + // 断言:验证行字段更新
75 MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId()); 82 MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId());
76 assertEquals(oqcId, updateLine.getOqcId()); 83 assertEquals(oqcId, updateLine.getOqcId());
77 assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), updateLine.getQualityStatus()); 84 assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), updateLine.getQualityStatus());
78 } 85 }
79 86
  87 + @Test
  88 + public void testUpdateProductSalesLineWhenOqcFinish_cancelWhenNg() {
  89 + // 准备数据:同一出库单下 2 行,第 1 行已合格,第 2 行即将变更为不合格
  90 + Long salesId = randomLongId();
  91 + MesWmProductSalesLineDO line1 = randomPojo(MesWmProductSalesLineDO.class, o -> {
  92 + o.setSalesId(salesId);
  93 + o.setOqcCheck(true);
  94 + o.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus());
  95 + });
  96 + productSalesLineMapper.insert(line1);
  97 +
  98 + MesWmProductSalesLineDO line2 = randomPojo(MesWmProductSalesLineDO.class, o -> {
  99 + o.setSalesId(salesId);
  100 + o.setOqcCheck(true);
  101 + o.setQualityStatus(null); // 待检
  102 + });
  103 + productSalesLineMapper.insert(line2);
  104 +
  105 + // 调用:第 2 行质检不合格
  106 + productSalesLineService.updateProductSalesLineWhenOqcFinish(
  107 + line2.getId(), randomLongId(), MesQcCheckResultEnum.FAIL.getType());
  108 +
  109 + // 断言:应该调用 cancelProductSales
  110 + verify(productSalesService).cancelProductSales(eq(salesId));
  111 + }
  112 +
  113 + @Test
  114 + public void testUpdateProductSalesLineWhenOqcFinish_noCancelWhenAllPass() {
  115 + // 准备数据:同一出库单下 2 行,第 1 行已合格,第 2 行即将变更为合格
  116 + Long salesId = randomLongId();
  117 + MesWmProductSalesLineDO line1 = randomPojo(MesWmProductSalesLineDO.class, o -> {
  118 + o.setSalesId(salesId);
  119 + o.setOqcCheck(true);
  120 + o.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus());
  121 + });
  122 + productSalesLineMapper.insert(line1);
  123 +
  124 + MesWmProductSalesLineDO line2 = randomPojo(MesWmProductSalesLineDO.class, o -> {
  125 + o.setSalesId(salesId);
  126 + o.setOqcCheck(true);
  127 + o.setQualityStatus(null);
  128 + });
  129 + productSalesLineMapper.insert(line2);
  130 +
  131 + // 调用:第 2 行质检合格
  132 + productSalesLineService.updateProductSalesLineWhenOqcFinish(
  133 + line2.getId(), randomLongId(), MesQcCheckResultEnum.PASS.getType());
  134 +
  135 + // 断言:不应该调用 cancelProductSales
  136 + verify(productSalesService, never()).cancelProductSales(anyLong());
  137 + }
  138 +
  139 + @Test
  140 + public void testUpdateProductSalesLineWhenOqcFinish_skipNonOqcCheckLines() {
  141 + // 准备数据:同一出库单下 2 行,第 1 行不需要 OQC 检验(oqcCheck=false),第 2 行需要检验且不合格
  142 + Long salesId = randomLongId();
  143 + MesWmProductSalesLineDO line1 = randomPojo(MesWmProductSalesLineDO.class, o -> {
  144 + o.setSalesId(salesId);
  145 + o.setOqcCheck(false); // 不需要 OQC 检验
  146 + o.setQualityStatus(null);
  147 + });
  148 + productSalesLineMapper.insert(line1);
  149 +
  150 + MesWmProductSalesLineDO line2 = randomPojo(MesWmProductSalesLineDO.class, o -> {
  151 + o.setSalesId(salesId);
  152 + o.setOqcCheck(true);
  153 + o.setQualityStatus(null);
  154 + });
  155 + productSalesLineMapper.insert(line2);
  156 +
  157 + // 调用:第 2 行质检不合格
  158 + productSalesLineService.updateProductSalesLineWhenOqcFinish(
  159 + line2.getId(), randomLongId(), MesQcCheckResultEnum.FAIL.getType());
  160 +
  161 + // 断言:第 2 行不合格 → 应该取消出库单
  162 + verify(productSalesService).cancelProductSales(eq(salesId));
  163 + }
  164 +
80 } 165 }