云
22 小时以前 704265308b1227dbada5a36b2027b588c062ded2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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));
    }
}