package cn.iocoder.yudao.module.mes.service.wm.productproduce; import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper; import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductProduceStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.math.BigDecimal; 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 MesWmProductProduceServiceImpl} 的单元测试 * * @author 芋道源码 */ @Import(MesWmProductProduceServiceImpl.class) public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest { @Resource private MesWmProductProduceServiceImpl productProduceService; @Resource private MesWmProductProduceMapper productProduceMapper; @MockitoBean private MesWmProductProduceLineService productProduceLineService; @MockitoBean private MesWmProductProduceDetailService productProduceDetailService; @MockitoBean private MesProWorkOrderService workOrderService; @MockitoBean private MesWmBatchService batchService; @MockitoBean private MesWmTransactionService wmTransactionService; @MockitoBean private MesWmWarehouseService warehouseService; @MockitoBean private MesWmWarehouseLocationService locationService; @MockitoBean private MesWmWarehouseAreaService areaService; @Test public void testSplitPendingAndFinishProduce_withUnqualified() { // 准备数据:插入一个 PREPARE 状态的产出单 Long feedbackId = randomLongId(); Long sourceLineId = 100L; Long itemId = randomLongId(); Long batchId = randomLongId(); String batchCode = "BATCH-001"; MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> { o.setFeedbackId(feedbackId); o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus()); }); productProduceMapper.insert(produce); // mock: 虚拟线边库 Long whId = randomLongId(), locId = randomLongId(), areaId = randomLongId(); MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(whId); MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(locId); MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(areaId); when(warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE)).thenReturn(wh); when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION)).thenReturn(loc); when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA)).thenReturn(area); // mock: 通过 sourceLineId 直接定位 PENDING 产出行 MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder() .id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId) .itemId(itemId).quantity(BigDecimal.valueOf(100)) .batchId(batchId).batchCode(batchCode) .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()) .build(); when(productProduceLineService.validateProductProduceLineExists(sourceLineId)) .thenReturn(pendingLine); // mock: finishProductProduce 内部查行和明细(用于创建库存事务) MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder() .id(100L).produceId(produce.getId()).itemId(itemId) .quantity(BigDecimal.valueOf(80)) .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus()) .build(); MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder() .id(101L).produceId(produce.getId()).itemId(itemId) .quantity(BigDecimal.valueOf(20)) .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus()) .build(); when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId())) .thenReturn(ListUtil.of(qualifiedLine, unqualifiedLine)); // mock: finishProductProduce 中按行查明细 MesWmProductProduceDetailDO qualifiedDetail = MesWmProductProduceDetailDO.builder() .lineId(100L).quantity(BigDecimal.valueOf(80)).build(); MesWmProductProduceDetailDO unqualifiedDetail = MesWmProductProduceDetailDO.builder() .lineId(101L).quantity(BigDecimal.valueOf(20)).build(); when(productProduceDetailService.getProductProduceDetailListByLineId(100L)) .thenReturn(ListUtil.of(qualifiedDetail)); when(productProduceDetailService.getProductProduceDetailListByLineId(101L)) .thenReturn(ListUtil.of(unqualifiedDetail)); // 调用 productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, BigDecimal.valueOf(80), BigDecimal.valueOf(20)); // 断言 1:不合格品行 - 新建了一行 ArgumentCaptor lineCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class); verify(productProduceLineService).createProductProduceLine(lineCaptor.capture()); MesWmProductProduceLineDO createdLine = lineCaptor.getValue(); assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), createdLine.getQualityStatus()); assertEquals(0, BigDecimal.valueOf(20).compareTo(createdLine.getQuantity())); assertEquals(itemId, createdLine.getItemId()); // 断言 2:原待检行 - 更新为合格品 ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class); verify(productProduceLineService).updateProductProduceLine(updateCaptor.capture()); MesWmProductProduceLineDO updatedLine = updateCaptor.getValue(); assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updatedLine.getQualityStatus()); assertEquals(0, BigDecimal.valueOf(80).compareTo(updatedLine.getQuantity())); // 断言 3:生成了 2 条明细(不合格品明细 + 合格品明细) verify(productProduceDetailService, times(2)).createProductProduceDetail(any()); // 断言 4:产出单状态更新为已完成 MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId()); assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus()); // 断言 5:创建了库存事务 verify(wmTransactionService, times(2)).createTransaction(any()); } @Test public void testSplitPendingAndFinishProduce_allQualified() { // 准备数据:全部合格品场景 Long feedbackId = randomLongId(); Long sourceLineId = 200L; Long itemId = randomLongId(); MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> { o.setFeedbackId(feedbackId); o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus()); }); productProduceMapper.insert(produce); // mock: 虚拟线边库 Long whId = randomLongId(), locId = randomLongId(), areaId = randomLongId(); MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(whId); MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(locId); MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(areaId); when(warehouseService.getWarehouseByCode(any())).thenReturn(wh); when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc); when(areaService.getWarehouseAreaByCode(any())).thenReturn(area); // mock: 通过 sourceLineId 直接定位 PENDING 产出行 MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder() .id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId) .itemId(itemId).quantity(BigDecimal.valueOf(50)) .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus()) .build(); when(productProduceLineService.validateProductProduceLineExists(sourceLineId)) .thenReturn(pendingLine); // mock: finishProductProduce 查行和明细 MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder() .id(200L).produceId(produce.getId()).itemId(itemId) .quantity(BigDecimal.valueOf(50)) .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus()) .build(); when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId())) .thenReturn(ListUtil.of(qualifiedLine)); MesWmProductProduceDetailDO detail = MesWmProductProduceDetailDO.builder() .lineId(200L).quantity(BigDecimal.valueOf(50)).build(); when(productProduceDetailService.getProductProduceDetailListByLineId(200L)) .thenReturn(ListUtil.of(detail)); // 调用:不合格品数量 = 0 productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, BigDecimal.valueOf(50), BigDecimal.ZERO); // 断言 1:没有新建行(不合格品数量为 0) verify(productProduceLineService, never()).createProductProduceLine(any()); // 断言 2:更新了原待检行为合格 ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class); verify(productProduceLineService).updateProductProduceLine(updateCaptor.capture()); assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateCaptor.getValue().getQualityStatus()); // 断言 3:只生成了 1 条明细(全部合格品) verify(productProduceDetailService, times(1)).createProductProduceDetail(any()); // 断言 4:产出单状态更新为已完成 MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId()); assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus()); } @Test public void testSplitPendingAndFinishProduce_produceNotExists() { // 调用不存在的 feedbackId,应该抛异常 Long feedbackId = randomLongId(); Long sourceLineId = randomLongId(); ServiceException ex = assertThrows(ServiceException.class, () -> productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, BigDecimal.TEN, BigDecimal.ZERO)); assertEquals(WM_PRODUCT_PRODUCE_NOT_EXISTS.getCode(), ex.getCode()); } @Test public void testSplitPendingAndFinishProduce_lineNotExists() { // 准备数据:产出单存在,但 sourceLineId 对应的行不存在 Long feedbackId = randomLongId(); Long sourceLineId = randomLongId(); MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> { o.setFeedbackId(feedbackId); o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus()); }); productProduceMapper.insert(produce); // mock: 虚拟线边库 MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(1L); MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(1L); MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(1L); when(warehouseService.getWarehouseByCode(any())).thenReturn(wh); when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc); when(areaService.getWarehouseAreaByCode(any())).thenReturn(area); // mock: validateProductProduceLineExists 抛异常 when(productProduceLineService.validateProductProduceLineExists(sourceLineId)) .thenThrow(new ServiceException(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS)); // 调用,应该抛异常 ServiceException ex = assertThrows(ServiceException.class, () -> productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, BigDecimal.TEN, BigDecimal.ZERO)); assertEquals(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS.getCode(), ex.getCode()); } }