修复海外散货托盘收货与短少入库闭环
This commit is contained in:
+24
@@ -348,6 +348,30 @@ class InboundPdaWorkflowServiceTest {
|
||||
assertEquals("UNASSIGNED", second.getStatus());
|
||||
}
|
||||
|
||||
/** 同票允许按实际打托超过单托建议数,同时保留整票箱数上限。 */
|
||||
@Test
|
||||
void cartonScanCanExceedPalletSuggestionButCannotExceedInboundTotal() {
|
||||
InboundPlanCarton carton = carton("BOX-REAL", "UNASSIGNED", targetLine.getId(), "WH-A");
|
||||
activeTask(); target.setPickedPackageCount(3);
|
||||
when(repository.lockCarton("100", "BOX-REAL")).thenReturn(carton);
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||
when(repository.lockAllocation(plan.getId(), targetLine.getId(), targetPallet.getId())).thenReturn(target);
|
||||
when(repository.scannedCartons(plan.getId(), targetLine.getId())).thenReturn(3L);
|
||||
|
||||
service.scanCarton("100", "LIP1", new PdaRequests.ScanCarton(
|
||||
"BOX-REAL", targetPallet.getId(), "actual-pallet", "7", "worker"));
|
||||
assertEquals(4, target.getPickedPackageCount());
|
||||
assertEquals(3, target.getPlannedPackageCount());
|
||||
|
||||
InboundPlanCarton extra = carton("BOX-EXTRA", "UNASSIGNED", targetLine.getId(), "WH-A");
|
||||
when(repository.lockCarton("100", "BOX-EXTRA")).thenReturn(extra);
|
||||
when(repository.scannedCartons(plan.getId(), targetLine.getId())).thenReturn(5L);
|
||||
assertThrows(ResponseStatusException.class, () -> service.scanCarton("100", "LIP1", new PdaRequests.ScanCarton(
|
||||
"BOX-EXTRA", targetPallet.getId(), "extra-pallet", "7", "worker")));
|
||||
assertEquals("UNASSIGNED", extra.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonScanRejectsDuplicateAndWrongDestination() {
|
||||
InboundPlanCarton repeated = carton("BOX-002", "ON_PALLET", targetLine.getId(), "WH-A");
|
||||
|
||||
+22
@@ -81,6 +81,28 @@ class InboundShortageConfirmationTest {
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirmShortage("100", "IP-1", "admin", "key", request(4, 19)));
|
||||
}
|
||||
|
||||
/** 普通海外仓逐箱货物确认短少后,已收箱可用、缺失箱保留,原现场时间不变。 */
|
||||
@Test void overseasShortageMakesOnlyReceivedCartonsAvailable() {
|
||||
line.setFbaWarehouseCode(null); line.setDestinationType("OVERSEAS");
|
||||
InboundPlanCarton received = new InboundPlanCarton();
|
||||
received.setId(20L); received.setStatus("ON_PALLET"); received.setVersion(1);
|
||||
InboundPlanCarton missing = new InboundPlanCarton();
|
||||
missing.setId(21L); missing.setStatus("UNASSIGNED"); missing.setVersion(0);
|
||||
when(repository.cartons(1L)).thenReturn(List.of(received, missing));
|
||||
|
||||
var result = service.confirmShortage("100", "IP-1", "admin", "overseas-shortage", request(4, 20));
|
||||
|
||||
assertEquals("PARTIAL_INBOUND", result.status());
|
||||
assertEquals("AVAILABLE", received.getStatus());
|
||||
assertEquals(receivedAt, received.getReceivedAt());
|
||||
assertEquals("UNASSIGNED", missing.getStatus());
|
||||
assertEquals(21, line.getRequiredPackageCount());
|
||||
verify(stock).stockIn(eq("100"), eq(plan), eq(task), eq(List.of(line)),
|
||||
eq(List.of(allocation)), anyList(), eq(receivedAt), eq(true));
|
||||
verify(repository).update(received); verify(repository, never()).update(missing);
|
||||
verifyNoInteractions(outbox);
|
||||
}
|
||||
|
||||
/** 页面旧版本或旧实收数不能确认库存。 */
|
||||
@Test void rejectsStaleVersionAndCounts() {
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirmShortage("100", "IP-1", "admin", "key", request(3, 20)));
|
||||
|
||||
+71
@@ -6,6 +6,8 @@ import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt;
|
||||
@@ -38,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -50,6 +53,7 @@ class InboundStockReceiptServiceTest {
|
||||
@Mock LastlegOrderDetailMapper orderDetails;
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock ReceivingSkuReceiptMapper receivingSkuReceipts;
|
||||
@Mock PdaWorkflowRepository workflowRepository;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@@ -57,6 +61,7 @@ class InboundStockReceiptServiceTest {
|
||||
private InboundStockReceiptService service;
|
||||
|
||||
@BeforeEach
|
||||
/** 组装库存、资源与逐箱事实的隔离依赖。 */
|
||||
void setUp() {
|
||||
service = new InboundStockReceiptService();
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "storageRent", org.mockito.Mockito.mock(com.baiyue.lastleg.storagerent.service.StorageRentService.class));
|
||||
@@ -64,6 +69,7 @@ class InboundStockReceiptServiceTest {
|
||||
ReflectionTestUtils.setField(service, "orderDetails", orderDetails);
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "receivingSkuReceipts", receivingSkuReceipts);
|
||||
ReflectionTestUtils.setField(service, "workflowRepository", workflowRepository);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
org.mockito.Mockito.lenient().when(warehouseGateway.resolveLocation(eq(900L), anyLong())).thenAnswer(invocation -> {
|
||||
@@ -174,6 +180,71 @@ class InboundStockReceiptServiceTest {
|
||||
assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum());
|
||||
}
|
||||
|
||||
/** 海外逐箱收货按箱码保留实际 SKU,短少时也能准确识别缺货商品。 */
|
||||
@Test
|
||||
void overseasShortageStocksTheScannedSkuInsteadOfTheFirstDeclaredSku() {
|
||||
Fixture fixture = fixture(4, 1, 1, 4);
|
||||
fixture.line.setDestinationType("OVERSEAS"); fixture.order.setDestinationType("OVERSEAS");
|
||||
fixture.detail.setPackageCount(2);
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(fixture.detail, detail(202L, "SKU-2", 2, 2)));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(workflowRepository.cartons(1L)).thenReturn(List.of(receivedCarton(1L, fixture.pallet, "SKU-2")));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 1, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true);
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> request = ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory).stockIn(eq("100"), eq("operator"), request.capture());
|
||||
assertEquals("SKU-2", request.getValue().skuCode());
|
||||
assertEquals(1L, request.getValue().packages());
|
||||
assertEquals(2001L, request.getValue().palletId());
|
||||
}
|
||||
|
||||
/** 同票现场调整打托后,两种 SKU 的实托归属仍由箱码决定。 */
|
||||
@Test
|
||||
void overseasCompleteReceiptKeepsReversedSkuPalletAssignments() {
|
||||
Fixture fixture = fixture(2, 1, 1, 2);
|
||||
fixture.line.setDestinationType("OVERSEAS"); fixture.detail.setPackageCount(1);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 1);
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(fixture.detail, detail(202L, "SKU-2", 1, 1)));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(workflowRepository.cartons(1L)).thenReturn(List.of(
|
||||
receivedCarton(1L, fixture.pallet, "SKU-2"), receivedCarton(2L, secondPallet, "SKU-1")));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 1, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation, secondAllocation), List.of(fixture.pallet, secondPallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> requests = ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory, times(2)).stockIn(eq("100"), eq("operator"), requests.capture());
|
||||
assertEquals(java.util.Set.of("SKU-2:2001", "SKU-1:2002"), requests.getAllValues().stream()
|
||||
.map(value -> value.skuCode() + ":" + value.palletId()).collect(java.util.stream.Collectors.toSet()));
|
||||
}
|
||||
|
||||
/** 缺少逐箱证据时拒绝库存入账,不按申报数量补出实收。 */
|
||||
@Test
|
||||
void overseasReceiptRequiresScannedCartonEvidence() {
|
||||
Fixture fixture = fixture(4, 1, 1, 4);
|
||||
fixture.line.setDestinationType("OVERSEAS");
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
assertThrows(ResponseStatusException.class, () -> service.stockIn("100", fixture.plan, fixture.task,
|
||||
List.of(fixture.line), List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true));
|
||||
verify(inventory, never()).stockIn(anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
/** 构造已扫描且绑定真实托盘的箱码事实。 */
|
||||
private InboundPlanCarton receivedCarton(long id, InboundPlanPallet pallet, String sku) {
|
||||
InboundPlanCarton carton = new InboundPlanCarton();
|
||||
carton.setId(id); carton.setCustomerId("100"); carton.setPlanId(1L); carton.setLineId(12L);
|
||||
carton.setOrderId(101L); carton.setPlanPalletId(pallet.getId()); carton.setSkuCode(sku);
|
||||
carton.setWarehousePalletId(pallet.getWarehousePalletId()); carton.setWarehouseLocationId(pallet.getWarehouseLocationId());
|
||||
carton.setStatus("ON_PALLET"); return carton;
|
||||
}
|
||||
|
||||
@Test
|
||||
void stocksInToHeldInventoryWhenInboundOrderIsDetained() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
|
||||
+23
-1
@@ -652,12 +652,34 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 非本票预留托盘在校验和提交阶段均拒绝,避免误报可收货。 */
|
||||
void overseasUnreservedPalletIsRejectedBeforeWarehouseLookup() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
when(cartons.selectOne(any())).thenReturn(carton(1L, "IN-1#1"));
|
||||
|
||||
assertThatThrownBy(() -> service.resolvePallet("7",
|
||||
new ReceivingContainerRequests.PalletResolve("PAL-OTHER", "IN-1")))
|
||||
.hasMessageContaining("Pallet code is not reserved for this inbound code");
|
||||
assertThatThrownBy(() -> service.receiveCarton("7", "receiver",
|
||||
new ReceivingContainerRequests.CartonReceive("IN-1#1", "PAL-OTHER", "wrong-pallet")))
|
||||
.hasMessageContaining("Pallet code is not reserved for this inbound code");
|
||||
verifyNoInteractions(warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 业务目的仓与接柜仓不同,仍按已预留的实际接柜仓校验。 */
|
||||
void overseasPalletUsesActualReceivingWarehouseInsteadOfBusinessDestinationId() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
InboundPlanLine overseasLine = line("OVERSEAS");
|
||||
overseasLine.setDestinationWarehouseId("900");
|
||||
when(lines.selectList(any())).thenReturn(List.of(overseasLine));
|
||||
when(warehouse.resolveAvailablePallet("PAL-1")).thenReturn(resource(100L, null));
|
||||
InboundPlanPallet reserved = planPallet();
|
||||
reserved.setDestinationWarehouseId("900"); reserved.setPalletNo("PAL-1");
|
||||
reserved.setWarehousePalletId(30L); reserved.setWarehouseLocationId(40L);
|
||||
reserved.setWarehouseResourceLockId(50L); reserved.setAssignmentStatus("RESERVED");
|
||||
when(pallets.selectList(any())).thenReturn(List.of(reserved));
|
||||
when(warehouse.resolveReservedPallet("PAL-1", "100", 100L, 30L, 40L, 50L))
|
||||
.thenReturn(resource(100L, null));
|
||||
|
||||
var resolved = service.resolvePallet("7",
|
||||
new ReceivingContainerRequests.PalletResolve("PAL-1", "IN-1"));
|
||||
|
||||
Reference in New Issue
Block a user