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 27 @Schema(description = "检测结果", example = "1")
28 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 30 @Schema(description = "检测指标类型", example = "QUANTITATIVE")
31 31 private String indicatorType;
32 32  
  33 + @Schema(description = "检测工具", example = "卡尺")
  34 + private String tool;
  35 +
33 36 @Schema(description = "检测方法", example = "目测")
34 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 99 // ========== 数量 ==========
100 100  
101 101 /**
  102 + * 最低检测数
  103 + */
  104 + private Integer minCheckQuantity;
  105 + /**
  106 + * 最大不合格数
  107 + */
  108 + private Integer maxUnqualifiedQuantity;
  109 + /**
102 110 * 本次出货数量
103 111 */
104 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 22 .likeIfPresent(MesQcOqcDO::getBatchCode, reqVO.getBatchCode())
23 23 .eqIfPresent(MesQcOqcDO::getItemId, reqVO.getItemId())
24 24 .eqIfPresent(MesQcOqcDO::getCheckResult, reqVO.getCheckResult())
25   - .eqIfPresent(MesQcOqcDO::getStatus, reqVO.getStatus())
26   - .eqIfPresent(MesQcOqcDO::getInspectorUserId, reqVO.getInspectorUserId())
27 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 75 public Long createOqc(MesQcOqcSaveReqVO createReqVO) {
76 76 // 1.1 校验编号唯一
77 77 validateOqcCodeUnique(null, createReqVO.getCode());
78   - // 1.2 校验客户、物料、检测人员存在
79   - clientService.validateClientExists(createReqVO.getClientId());
  78 + // 1.2 校验物料、检测人员存在
80 79 itemService.validateItemExists(createReqVO.getItemId());
81 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 87 MesQcTemplateItemDO templateItem = templateDetailService.getRequiredTemplateByItemIdAndType(
84 88 createReqVO.getItemId(), MesQcTypeEnum.OQC.getType());
85 89 Long templateId = templateItem.getTemplateId();
86   - // 1.4 获取来源单据编号
87   - String sourceDocCode = validateAndGetSourceDocCode(
88   - createReqVO.getSourceDocType(), createReqVO.getSourceLineId());
89 90  
90 91 // 2. 插入主表
91 92 MesQcOqcDO oqc = BeanUtils.toBean(createReqVO, MesQcOqcDO.class)
... ... @@ -137,7 +138,9 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
137 138 /**
138 139 * 回写来源单据(OQC 完成后)
139 140 *
140   - * <p>当来源为销售出库单时,回写行的 oqcId 和 qualityStatus
  141 + * <p>当来源为销售出库单时,调用
  142 + * {@link MesWmProductSalesLineService#updateProductSalesLineWhenOqcFinish}
  143 + * 回写行的 oqcId、qualityStatus,并由该方法内部联动检查出库单整体状态
141 144 *
142 145 * @param oqc 出货检验单
143 146 */
... ... @@ -151,8 +154,8 @@ public class MesQcOqcServiceImpl implements MesQcOqcService {
151 154 }
152 155  
153 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 159 } else {
157 160 throw new IllegalArgumentException(
158 161 "OQC 单[" + oqc.getId() + "] sourceDocType=" + oqc.getSourceDocType() + " 无法识别,无法回写来源单据");
... ... @@ -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 216 private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceLineId) {
207 217 if (sourceDocType == null || sourceLineId == null) {
208 218 return null;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java
1 1 package cn.iocoder.yudao.module.mes.service.wm.productsales;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
3 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 5 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.line.MesWmProductSalesLinePageReqVO;
5 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 10 import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
10 11 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
11 12 import jakarta.annotation.Resource;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.context.annotation.Lazy;
12 15 import org.springframework.stereotype.Service;
13 16 import org.springframework.transaction.annotation.Transactional;
14 17 import org.springframework.validation.annotation.Validated;
... ... @@ -26,6 +29,7 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_SA
26 29 */
27 30 @Service
28 31 @Validated
  32 +@Slf4j
29 33 public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineService {
30 34  
31 35 @Resource
... ... @@ -34,6 +38,9 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe
34 38 private MesWmProductSalesDetailService productSalesDetailService;
35 39 @Resource
36 40 private MesMdItemService itemService;
  41 + @Resource
  42 + @Lazy
  43 + private MesWmProductSalesService productSalesService;
37 44  
38 45 @Override
39 46 public Long createProductSalesLine(MesWmProductSalesLineSaveReqVO createReqVO) {
... ... @@ -100,11 +107,29 @@ public class MesWmProductSalesLineServiceImpl implements MesWmProductSalesLineSe
100 107 }
101 108  
102 109 @Override
  110 + @Transactional(rollbackFor = Exception.class)
103 111 public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) {
  112 + // 1. 更新行的 oqcId + qualityStatus
104 113 Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.PASS.getType())
105 114 ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus();
106 115 productSalesLineMapper.updateById(new MesWmProductSalesLineDO()
107 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 5 import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
6 6 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
7 7 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
  8 +import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
8 9 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
9 10 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
10 11 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateDetailService;
11 12 import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService;
  13 +import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
12 14 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
13 15 import jakarta.annotation.Resource;
14 16 import org.junit.jupiter.api.Test;
... ... @@ -18,11 +20,10 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
18 20 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
19 21 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
20 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 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 29 * {@link MesQcOqcServiceImpl} 的单元测试类
... ... @@ -41,8 +42,12 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
41 42 @MockitoBean
42 43 private MesWmProductSalesLineService productSalesLineService;
43 44 @MockitoBean
  45 + private MesWmProductSalesService productSalesService;
  46 + @MockitoBean
44 47 private MesQcOqcLineService oqcLineService;
45 48 @MockitoBean
  49 + private MesMdClientService clientService;
  50 + @MockitoBean
46 51 private MesMdItemService itemService;
47 52 @MockitoBean
48 53 private MesQcTemplateDetailService templateDetailService;
... ... @@ -52,12 +57,10 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
52 57 private AdminUserApi adminUserApi;
53 58  
54 59 @Test
55   - public void testFinishOqc_success() {
56   - // 准备参数
  60 + public void testFinishOqc_writeBack_productSales() {
  61 + // 准备数据:插入一条草稿状态的 OQC 单,来源为销售出库单
57 62 Long sourceLineId = randomLongId();
58 63 Integer checkResult = 1;
59   -
60   - // mock 数据
61 64 MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
62 65 o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
63 66 o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
... ... @@ -69,12 +72,64 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
69 72 // 调用
70 73 oqcService.finishOqc(oqc.getId());
71 74  
72   - // 断言:验证状态更新
  75 + // 断言:验证状态更新为已完成
73 76 MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
74 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 135 @Test
... ... @@ -88,8 +143,7 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
88 143  
89 144 @Test
90 145 public void testFinishOqc_statusNotDraft() {
91   - // 准备参数
92   - // mock 数据:状态为已完成
  146 + // 准备参数:状态为已完成
93 147 MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
94 148 o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
95 149 });
... ... @@ -99,4 +153,48 @@ public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
99 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 15 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
16 16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
17 17 import static org.junit.jupiter.api.Assertions.assertEquals;
  18 +import static org.mockito.Mockito.*;
18 19  
19 20 /**
20 21 * {@link MesWmProductSalesLineServiceImpl} 的单元测试类
... ... @@ -46,13 +47,16 @@ public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest {
46 47 Integer checkResult = MesQcCheckResultEnum.PASS.getType();
47 48  
48 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 54 productSalesLineMapper.insert(lineDO);
51 55  
52 56 // 调用
53 57 productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult);
54 58  
55   - // 断言:验证 mapper 更新参数正确
  59 + // 断言:验证行字段更新
56 60 MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId());
57 61 assertEquals(oqcId, updateLine.getOqcId());
58 62 assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateLine.getQualityStatus());
... ... @@ -65,16 +69,97 @@ public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest {
65 69 Integer checkResult = MesQcCheckResultEnum.FAIL.getType();
66 70  
67 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 76 productSalesLineMapper.insert(lineDO);
70 77  
71 78 // 调用
72 79 productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult);
73 80  
74   - // 断言:验证 mapper 更新参数正确
  81 + // 断言:验证行字段更新
75 82 MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId());
76 83 assertEquals(oqcId, updateLine.getOqcId());
77 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 }
... ...