package cn.iocoder.yudao.module.mes.service.qc.ipqc;
|
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
|
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
|
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
|
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
|
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
|
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
|
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
|
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
import jakarta.annotation.Resource;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.context.annotation.Import;
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
import java.math.BigDecimal;
|
import java.util.List;
|
|
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
import static org.mockito.Mockito.*;
|
|
/**
|
* {@link MesQcIpqcServiceImpl} 的单元测试
|
*
|
* @author 超级管理员
|
*/
|
@Import(MesQcIpqcServiceImpl.class)
|
public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
|
|
@Resource
|
private MesQcIpqcServiceImpl ipqcService;
|
|
@Resource
|
private MesQcIpqcMapper ipqcMapper;
|
|
@MockitoBean
|
private MesQcTemplateItemService templateItemService;
|
@MockitoBean
|
private MesQcIpqcLineService ipqcLineService;
|
@MockitoBean
|
private MdmItemApi mdmItemApi;
|
@MockitoBean
|
private MesQcDefectRecordService defectRecordService;
|
@MockitoBean
|
private AdminUserApi adminUserApi;
|
@MockitoBean
|
private MesProFeedbackService feedbackService;
|
@MockitoBean
|
private MesQcIndicatorResultService indicatorResultService;
|
@MockitoBean
|
private MesWmProductProduceLineService productProduceLineService;
|
@MockitoBean
|
private MesQcNcrService ncrService;
|
|
@Test
|
public void testFinishIpqc_writeBack_feedback() {
|
// 准备数据:插入一条草稿状态的 IPQC 单,来源为报工单(PRO_FEEDBACK=304)
|
Long sourceDocId = randomLongId();
|
Long sourceLineId = randomLongId();
|
BigDecimal qualifiedQty = BigDecimal.valueOf(80);
|
BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
|
BigDecimal laborScrapQty = BigDecimal.valueOf(5);
|
BigDecimal materialScrapQty = BigDecimal.valueOf(10);
|
BigDecimal otherScrapQty = BigDecimal.valueOf(5);
|
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
|
o.setCheckResult(1); // 非空,满足 finishIpqc 的校验
|
o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
|
o.setSourceDocId(sourceDocId);
|
o.setSourceLineId(sourceLineId);
|
o.setQualifiedQuantity(qualifiedQty);
|
o.setUnqualifiedQuantity(unqualifiedQty);
|
o.setLaborScrapQuantity(laborScrapQty);
|
o.setMaterialScrapQuantity(materialScrapQty);
|
o.setOtherScrapQuantity(otherScrapQty);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用
|
ipqcService.finishIpqc(ipqc.getId());
|
|
// 断言 1:状态更新为已完成
|
MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
|
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
|
|
// 断言 2:调用了 feedbackService,传递 sourceLineId
|
// 注意:数量经过 DB 存取后 scale 可能变化(例如 80 → 80.00),所以用 any() 匹配
|
verify(feedbackService).updateProFeedbackWhenIpqcFinish(
|
eq(sourceDocId),
|
eq(sourceLineId),
|
any(BigDecimal.class),
|
any(BigDecimal.class),
|
any(BigDecimal.class),
|
any(BigDecimal.class),
|
any(BigDecimal.class));
|
}
|
|
@Test
|
public void testFinishIpqc_writeBack_feedback_withNullQuantities() {
|
// 准备数据:合格品/不合格品/废品数量为 null 的场景,应该 defaultIfNull 为 0
|
Long sourceDocId = randomLongId();
|
Long sourceLineId = randomLongId();
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
|
o.setCheckResult(1);
|
o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
|
o.setSourceDocId(sourceDocId);
|
o.setSourceLineId(sourceLineId);
|
o.setQualifiedQuantity(null);
|
o.setUnqualifiedQuantity(null);
|
o.setLaborScrapQuantity(null);
|
o.setMaterialScrapQuantity(null);
|
o.setOtherScrapQuantity(null);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用
|
ipqcService.finishIpqc(ipqc.getId());
|
|
// 断言:所有数量参数都是 BigDecimal.ZERO(defaultIfNull 处理)
|
verify(feedbackService).updateProFeedbackWhenIpqcFinish(
|
eq(sourceDocId),
|
eq(sourceLineId),
|
eq(BigDecimal.ZERO),
|
eq(BigDecimal.ZERO),
|
eq(BigDecimal.ZERO),
|
eq(BigDecimal.ZERO),
|
eq(BigDecimal.ZERO));
|
}
|
|
@Test
|
public void testFinishIpqc_writeBack_noSourceDoc() {
|
// 准备数据:sourceDocType 为空,不需要回写
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
|
o.setCheckResult(1);
|
o.setSourceDocType(null);
|
o.setSourceDocId(null);
|
o.setSourceLineId(null);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用
|
ipqcService.finishIpqc(ipqc.getId());
|
|
// 断言 1:状态更新成功
|
MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
|
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
|
|
// 断言 2:不应该调用 feedbackService
|
verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
|
anyLong(), anyLong(), any(), any(), any(), any(), any());
|
}
|
|
@Test
|
public void testFinishIpqc_writeBack_sourceDocTypeDirtyData() {
|
// 准备数据:sourceDocType 非空但 sourceDocId 为空,跳过回写
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
|
o.setCheckResult(1);
|
o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
|
o.setSourceDocId(null); // 脏数据
|
o.setSourceLineId(null);
|
o.setUnqualifiedQuantity(BigDecimal.ZERO);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用:不抛异常,仅跳过回写
|
ipqcService.finishIpqc(ipqc.getId());
|
|
MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
|
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
|
verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
|
anyLong(), anyLong(), any(), any(), any(), any(), any());
|
}
|
|
@Test
|
public void testFinishIpqc_writeBack_unknownSourceDocType() {
|
// 准备数据:sourceDocType 无法识别,回写阶段不校验类型(仅按 sourceDocId 回写报工)
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
|
o.setCheckResult(1);
|
o.setSourceDocType(99999); // 无法识别的类型
|
o.setSourceDocId(randomLongId());
|
o.setSourceLineId(randomLongId());
|
o.setUnqualifiedQuantity(BigDecimal.ZERO);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用:按 sourceDocId 回写报工
|
ipqcService.finishIpqc(ipqc.getId());
|
|
MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
|
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
|
verify(feedbackService).updateProFeedbackWhenIpqcFinish(
|
anyLong(), anyLong(), any(), any(), any(), any(), any());
|
}
|
|
@Test
|
public void testFinishIpqc_checkResultEmpty() {
|
// 准备数据:checkResult 为空,应该报错
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
|
o.setCheckResult(null); // 检测结论为空
|
o.setSourceDocType(null);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用,应该抛异常(检测结论必填)
|
assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
|
}
|
|
@Test
|
public void testFinishIpqc_notDraftStatus() {
|
// 准备数据:已完成状态,不能再次完成
|
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
|
o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
|
o.setCheckResult(1);
|
});
|
ipqcMapper.insert(ipqc);
|
|
// 调用,应该抛异常(不是草稿状态)
|
assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
|
}
|
|
// ==================== 批号必填校验 ====================
|
|
/**
|
* 批号为 null → 创建失败
|
*/
|
@Test
|
public void testCreateIpqc_batchCode_null() {
|
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
|
reqVO.setCode("IPQC-BC-001");
|
reqVO.setBatchCode(null);
|
reqVO.setItemId(randomLongId());
|
reqVO.setInspectorUserId(randomLongId());
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
|
assertEquals(QC_IPQC_BATCH_CODE_REQUIRED.getCode(), ex.getCode());
|
}
|
|
/**
|
* 批号为空白串 → 创建失败(兜底内部/RPC 调用绕过 VO 校验的情况)
|
*/
|
@Test
|
public void testCreateIpqc_batchCode_blank() {
|
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
|
reqVO.setCode("IPQC-BC-002");
|
reqVO.setBatchCode(" ");
|
reqVO.setItemId(randomLongId());
|
reqVO.setInspectorUserId(randomLongId());
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
|
assertEquals(QC_IPQC_BATCH_CODE_REQUIRED.getCode(), ex.getCode());
|
}
|
|
// ==================== 创建成功:批号 / 物料落库 ====================
|
|
@Test
|
public void testCreateIpqc_success() {
|
// 准备参数
|
Long itemId = randomLongId();
|
Long templateId = randomLongId();
|
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
|
reqVO.setCode("IPQC-OK-001");
|
reqVO.setName("测试过程检验");
|
reqVO.setBatchCode("BC-2025-001");
|
reqVO.setItemId(itemId);
|
reqVO.setInspectorUserId(randomLongId());
|
mockTemplate(itemId, templateId);
|
|
// 调用
|
Long ipqcId = ipqcService.createIpqc(reqVO);
|
|
// 断言:批号与物料落库,模板按物料+IPQC 类型匹配
|
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
|
assertNotNull(ipqc);
|
assertEquals("BC-2025-001", ipqc.getBatchCode());
|
assertEquals(itemId, ipqc.getItemId());
|
assertEquals(templateId, ipqc.getTemplateId());
|
verify(mdmItemApi).validateItemExists(itemId);
|
}
|
|
/**
|
* 传入了不在匹配列表中的 templateId → 拒绝创建
|
*/
|
@Test
|
public void testCreateIpqc_templateId_notMatched() {
|
Long itemId = randomLongId();
|
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
|
reqVO.setCode("IPQC-TPL-001");
|
reqVO.setName("指定方案不匹配");
|
reqVO.setBatchCode("BC-2025-002");
|
reqVO.setItemId(itemId);
|
reqVO.setInspectorUserId(randomLongId());
|
reqVO.setTemplateId(randomLongId()); // 与匹配结果不一致
|
mockTemplate(itemId, randomLongId());
|
|
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
|
assertEquals(QC_NO_TEMPLATE.getCode(), ex.getCode());
|
}
|
|
// ==================== sourceLineId 校验测试 ====================
|
|
/**
|
* sourceDocType=PRO_FEEDBACK 时 sourceLineId 为空 → 创建失败
|
*/
|
@Test
|
public void testCreateIpqc_sourceLineId_required() {
|
Long itemId = randomLongId();
|
Long feedbackId = randomLongId();
|
MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-001", itemId, feedbackId, null);
|
mockTemplate(itemId, randomLongId());
|
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
|
|
// 调用,断言异常
|
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
|
assertEquals(QC_IPQC_SOURCE_LINE_REQUIRED.getCode(), ex.getCode());
|
}
|
|
/**
|
* sourceLineId 不属于该报工的产出行 → 创建失败
|
*/
|
@Test
|
public void testCreateIpqc_sourceLineId_notBelong() {
|
Long itemId = randomLongId();
|
Long feedbackId = randomLongId();
|
Long sourceLineId = randomLongId();
|
MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-002", itemId, feedbackId, sourceLineId);
|
mockTemplate(itemId, randomLongId());
|
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
|
// mock:产出行存在但 feedbackId 不匹配
|
MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
|
line.setId(sourceLineId);
|
line.setFeedbackId(randomLongId()); // 不同的 feedbackId
|
line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus());
|
when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
|
|
// 调用,断言异常
|
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
|
assertEquals(QC_IPQC_SOURCE_LINE_NOT_BELONG.getCode(), ex.getCode());
|
}
|
|
/**
|
* sourceLineId 对应的产出行不是待检验状态 → 创建失败
|
*/
|
@Test
|
public void testCreateIpqc_sourceLineId_notPending() {
|
Long itemId = randomLongId();
|
Long feedbackId = randomLongId();
|
Long sourceLineId = randomLongId();
|
MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-003", itemId, feedbackId, sourceLineId);
|
mockTemplate(itemId, randomLongId());
|
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
|
// mock:产出行存在且归属正确,但不是 PENDING 状态
|
MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
|
line.setId(sourceLineId);
|
line.setFeedbackId(feedbackId);
|
line.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); // 已合格,非待检
|
when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
|
|
// 调用,断言异常
|
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
|
assertEquals(QC_IPQC_SOURCE_LINE_NOT_PENDING.getCode(), ex.getCode());
|
}
|
|
/**
|
* sourceLineId 校验全部通过 → 创建成功
|
*/
|
@Test
|
public void testCreateIpqc_sourceLineId_success() {
|
Long itemId = randomLongId();
|
Long feedbackId = randomLongId();
|
Long sourceLineId = randomLongId();
|
MesQcIpqcSaveReqVO reqVO = buildFeedbackReqVO("IPQC-SL-004", itemId, feedbackId, sourceLineId);
|
mockTemplate(itemId, randomLongId());
|
MesProFeedbackDO feedback = new MesProFeedbackDO();
|
feedback.setId(feedbackId);
|
feedback.setCode("FB-001");
|
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(feedback);
|
// mock:产出行存在、归属正确、PENDING 状态
|
MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
|
line.setId(sourceLineId);
|
line.setFeedbackId(feedbackId);
|
line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus());
|
when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
|
|
// 调用
|
Long ipqcId = ipqcService.createIpqc(reqVO);
|
|
// 断言:创建成功,sourceDocCode 回写
|
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
|
assertNotNull(ipqc);
|
assertEquals(feedbackId, ipqc.getSourceDocId());
|
assertEquals(sourceLineId, ipqc.getSourceLineId());
|
assertEquals("FB-001", ipqc.getSourceDocCode());
|
}
|
|
// ==================== 测试辅助 ====================
|
|
private MesQcIpqcSaveReqVO buildFeedbackReqVO(String code, Long itemId, Long feedbackId, Long sourceLineId) {
|
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
|
reqVO.setCode(code);
|
reqVO.setName("来源校验过程检验");
|
reqVO.setBatchCode("BC-SL-001");
|
reqVO.setItemId(itemId);
|
reqVO.setInspectorUserId(randomLongId());
|
reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
|
reqVO.setSourceDocId(feedbackId);
|
reqVO.setSourceLineId(sourceLineId);
|
return reqVO;
|
}
|
|
private void mockTemplate(Long itemId, Long templateId) {
|
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
|
templateItem.setTemplateId(templateId);
|
when(templateItemService.getTemplateItemList(eq(itemId), anyInt()))
|
.thenReturn(List.of(templateItem));
|
}
|
|
}
|