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<MesWmMaterialStockBagDO> 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));
|
}
|
}
|