package cn.iocoder.yudao.module.mes.service.wm.productsales; import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.*; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.*; import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.*; import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService; import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService; import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import org.junit.jupiter.api.*; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class SalesPickingSelectionTest { final MesWmProductSalesServiceImpl service = new MesWmProductSalesServiceImpl(); final MesWmProductSalesMapper sales = mock(MesWmProductSalesMapper.class); final MesWmProductSalesDetailMapper details = mock(MesWmProductSalesDetailMapper.class); final MesProBagCodeMapper bags = mock(MesProBagCodeMapper.class); final MesProPalletMapper pallets = mock(MesProPalletMapper.class); final MesProBatchMapper batches = mock(MesProBatchMapper.class); final MesWmMaterialStockBagMapper inventory = mock(MesWmMaterialStockBagMapper.class); final MesProBagCodeService bagService = mock(MesProBagCodeService.class); final MesProPalletService palletService = mock(MesProPalletService.class); final MesProBatchService batchService = mock(MesProBatchService.class); final MesWmBatchService wmBatches = mock(MesWmBatchService.class); final MesWmMaterialStockService stocks = mock(MesWmMaterialStockService.class); final MesWmProductSalesLineService lines = mock(MesWmProductSalesLineService.class); final MesWmProductSalesDetailService detailService = mock(MesWmProductSalesDetailService.class); MesProBatchDO batch; MesProBagCodeDO b1, b2, b3; List stockBags; MesWmProductSalesScanReqVO req; @BeforeEach void setup() { Object[][] fields = {{"productSalesMapper",sales},{"productSalesDetailMapper",details},{"pickingBagMapper",bags}, {"pickingPalletMapper",pallets},{"pickingBatchMapper",batches},{"pickingStockBagMapper",inventory}, {"bagCodeService",bagService},{"palletService",palletService},{"proBatchService",batchService}, {"mesWmBatchService",wmBatches},{"materialStockService",stocks},{"productSalesLineService",lines},{"productSalesDetailService",detailService}}; for (var f : fields) ReflectionTestUtils.setField(service,(String)f[0],f[1]); batch = new MesProBatchDO().setId(100L).setCode("PRO-100").setItemId(1L).setStatus(30); b1=bag(31L); b2=bag(32L); b3=bag(33L); var pallet = new MesProPalletDO().setId(200L).setBatchId(100L).setStatus(10); when(sales.selectByIdForUpdate(11L)).thenReturn(new MesWmProductSalesDO().setId(11L).setStatus(2)); when(sales.selectById(11L)).thenReturn(new MesWmProductSalesDO().setId(11L).setStatus(2)); when(batchService.getBatch(100L)).thenReturn(batch); when(batchService.getBatchByScanCode("SOURCE")).thenReturn(batch); when(batches.selectByScanCodeForUpdate("SOURCE")).thenReturn(batch); when(pallets.selectByScanCodeForUpdate("SOURCE")).thenReturn(pallet); when(palletService.getPalletByScanCode("SOURCE")).thenReturn(pallet); when(bags.selectListByPalletIdForUpdate(200L)).thenReturn(List.of(b1,b2,b3)); when(bags.selectListByBatchIdForUpdate(100L)).thenReturn(List.of(b1,b2,b3)); when(bagService.getBagCodeListByPalletId(200L)).thenReturn(List.of(b1,b2,b3)); when(bagService.getBagCodeListByBatchId(100L)).thenReturn(List.of(b1,b2,b3)); when(wmBatches.getBatchByCode("PRO-100")).thenReturn(new MesWmBatchDO().setId(500L).setCode("PRO-100")); stockBags = List.of(stockBag(31L,900L),stockBag(32L,901L),stockBag(33L,900L)); when(inventory.selectByBagIds(anyCollection(),anyBoolean())).thenAnswer(i -> stockBags.stream().filter(b -> ((Collection)i.getArgument(0)).contains(b.getBagCodeId())).toList()); when(stocks.getQualifiedMaterialStockListByItemAndBatch(1L,500L)).thenReturn(List.of(stock(900L,8L),stock(901L,9L))); when(lines.getProductSalesLineListBySalesId(11L)).thenReturn(List.of(new MesWmProductSalesLineDO().setId(21L).setItemId(1L).setQuantity(BigDecimal.TEN))); req = new MesWmProductSalesScanReqVO(); req.setSalesId(11L);req.setScanContent("SOURCE");req.setScanType("PALLET"); req.setBagIds(List.of(31L,32L));req.setQuantity(BigDecimal.valueOf(2)); } MesProBagCodeDO bag(Long id) { return new MesProBagCodeDO().setId(id).setCode("TRACE-"+id).setBusinessCode("BUS-"+id).setBatchId(100L).setPalletId(200L).setStatus(20).setQcStatus(20); } MesWmMaterialStockBagDO stockBag(Long bagId,Long stockId) { return MesWmMaterialStockBagDO.builder().bagCodeId(bagId).materialStockId(stockId).itemId(1L).batchId(500L).status(1).stockState(20).frozen(false).quantity(BigDecimal.ONE).build(); } MesWmMaterialStockDO stock(Long id,Long warehouse) { return new MesWmMaterialStockDO().setId(id).setWarehouseId(warehouse).setQuantity(BigDecimal.TEN); } void noInsert() { verify(details,never()).insert(any(MesWmProductSalesDetailDO.class)); } @Test void partialPalletWritesExactlySelectedBagsAtTheirActualLocations() { var result=service.scanProductSales(req); var c=ArgumentCaptor.forClass(MesWmProductSalesDetailDO.class);verify(details,times(2)).insert(c.capture()); assertEquals(List.of(31L,32L),c.getAllValues().stream().map(MesWmProductSalesDetailDO::getBagCodeId).toList()); assertEquals(List.of(8L,9L),c.getAllValues().stream().map(MesWmProductSalesDetailDO::getWarehouseId).toList()); assertTrue(c.getAllValues().stream().allMatch(d -> BigDecimal.ONE.equals(d.getQuantity()) && d.getRemark().contains("TRACE-"))); assertEquals(BigDecimal.valueOf(2),result.getOutboundQuantity()); } @Test void partialBatchAlsoWritesPerBag() { req.setScanType("BATCH");var r=service.scanProductSales(req); assertEquals("BATCH",r.getScanType());verify(details,times(2)).insert(any(MesWmProductSalesDetailDO.class)); } @Test void scopeAndQuantityForgeryAreRejected() { req.setBagIds(List.of(31L,999L));assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req)); req.setBagIds(List.of(31L,32L));req.setQuantity(BigDecimal.ONE);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); } @Test void duplicateOrMissingSelectionIsRejected() { req.setBagIds(List.of(31L,31L));assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req)); req.setBagIds(null);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); } @Test void unqualifiedUnselectedBagDoesNotBlockPartialPick() { b3.setQcStatus(10);assertFalse(service.scanProductSales(req).getAlreadyScanned()); } @Test void selectedUnqualifiedFrozenOrOutboundBagBlocksEntireRequest() { b2.setQcStatus(10);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));b2.setQcStatus(20); stockBags.get(1).setFrozen(true);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));stockBags.get(1).setFrozen(false); stockBags.get(1).setStatus(2);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); } @Test void crossOrderAndLegacyWholePalletOverlapAreRejected() { when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(new MesWmProductSalesDetailDO().setSalesId(12L).setBagCodeId(31L))); assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req)); when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(new MesWmProductSalesDetailDO().setSalesId(11L).setPalletCodeId(200L))); assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); } @Test void sameExactBagsAreIdempotentButPartialRepeatIsRejected() { var d1=new MesWmProductSalesDetailDO().setSalesId(11L).setBagCodeId(31L);var d2=new MesWmProductSalesDetailDO().setSalesId(11L).setBagCodeId(32L); when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(d1,d2));assertTrue(service.scanProductSales(req).getAlreadyScanned()); when(details.selectPickingByBatchCodeForUpdate("PRO-100")).thenReturn(List.of(d1));assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));noInsert(); } @Test void previewReportsAvailabilityWithoutWrites() { b3.setQcStatus(10);stockBags.get(1).setFrozen(true); var r=service.previewScan("SOURCE","BATCH",11L);assertEquals(BigDecimal.ONE,r.getScanQuantity());assertEquals(3,r.getBagCount()); assertTrue(r.getBags().get(0).getSelectable());assertEquals("袋码已冻结",r.getBags().get(1).getUnavailableReason());noInsert();verify(wmBatches,never()).getOrCreateByProBatch(any()); } @Test void canceledBatchInvalidTypeAndPlanOverflowRejectWrites() { batch.setStatus(40);assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));batch.setStatus(30); req.setScanType("OTHER");assertThrows(IllegalArgumentException.class,()->service.scanProductSales(req));req.setScanType("BATCH"); when(lines.getProductSalesLineListBySalesId(11L)).thenReturn(List.of(new MesWmProductSalesLineDO().setId(21L).setItemId(1L).setQuantity(BigDecimal.ONE))); assertThrows(RuntimeException.class,()->service.scanProductSales(req));noInsert(); } @Test void aggregateStockInsufficientRejectsAllRows() { when(stocks.getQualifiedMaterialStockListByItemAndBatch(1L,500L)).thenReturn(List.of(stock(900L,8L).setQuantity(BigDecimal.ZERO),stock(901L,9L))); assertThrows(RuntimeException.class,()->service.scanProductSales(req));noInsert(); } @Test void detailResponseKeepsBagAndPalletAssociationsForRecovery() { var source=new MesWmProductSalesDetailDO().setBagCodeId(31L).setPalletCodeId(200L).setQuantity(BigDecimal.ONE); var response=cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(source,cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.detail.MesWmProductSalesDetailRespVO.class); assertEquals(31L,response.getBagCodeId());assertEquals(200L,response.getPalletCodeId()); } @Test void secondInsertFailureRollsBackAllSelectedBagDetails() { var source=new org.h2.jdbcx.JdbcDataSource();source.setURL("jdbc:h2:mem:pick"+UUID.randomUUID()+";DB_CLOSE_DELAY=-1"); var jdbc=new org.springframework.jdbc.core.JdbcTemplate(source);jdbc.execute("CREATE TABLE picking(bag_id BIGINT)"); doAnswer(i->{MesWmProductSalesDetailDO d=i.getArgument(0);jdbc.update("INSERT INTO picking VALUES (?)",d.getBagCodeId());if(d.getBagCodeId()==32L)throw new IllegalStateException("第二袋写入失败");return 1;}).when(details).insert(any(MesWmProductSalesDetailDO.class)); var factory=new org.springframework.aop.framework.ProxyFactory(service); factory.addAdvice(new org.springframework.transaction.interceptor.TransactionInterceptor(new org.springframework.jdbc.datasource.DataSourceTransactionManager(source),new org.springframework.transaction.annotation.AnnotationTransactionAttributeSource())); var proxy=(MesWmProductSalesService)factory.getProxy(); assertThrows(IllegalStateException.class,()->proxy.scanProductSales(req));assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM picking",Integer.class)); } }