贴标申请,仓储转运

This commit is contained in:
2026-09-17 17:37:29 +08:00
parent 3b8b6f5aa2
commit 29f05b7b09
106 changed files with 5019 additions and 471 deletions
@@ -25,7 +25,7 @@ class PdaPalletOperationControllerContractTest {
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
assertEquals(Set.of("/pallets/resolve", "/relabel-pallet-label", "/assemble", "/disassemble",
"/relabel-disassemble", "/swap", "/unbind"), paths);
"/relabel-disassemble", "/relabel-sources", "/swap", "/unbind"), paths);
}
@Test void palletContentAnnotationUsesNativeGreaterThanOperator() throws Exception {
@@ -42,6 +42,17 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PdaPalletOperationServiceTest {
@Test void relabelRebindingRejectsDispatchAndOutboundOccupation() {
RelabelApplication app = new RelabelApplication();
for (String status : List.of("PRE_DISPATCH_CREATED", "OUTBOUND_CREATED", "OUTBOUNDED")) {
app.setFulfillmentStatus(status);
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
}
app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
assertDoesNotThrow(() -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
app.setOutboundOrderNo("OUT-1");
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
}
@Mock private PdaPalletOperationMapper mapper;
@Mock private InventoryPositionMapper positions;
@Mock private InventoryBalanceMapper balances;
@@ -185,6 +196,13 @@ class PdaPalletOperationServiceTest {
}
@Test void relabelDisassembleMovesOnlySelectedHeldCartonWithoutRequiringLocations() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
ReflectionTestUtils.setField(service, "relabelWorkHistory", histories);
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();
task.setId(900L); task.setStatus("COMPLETED");
when(tasks.find("100", 600L)).thenReturn(task);
var sourcePallet = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, null,
"OCCUPIED", null, 1);
var targetPallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null,
@@ -196,7 +214,7 @@ class PdaPalletOperationServiceTest {
RelabelApplication application = new RelabelApplication(); application.setId(600L);
application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setStatus("READY_FOR_OUTBOUND");
application.setOverseasWarehouseId("88"); application.setOutboundOrderNo("OUT-1");
application.setOverseasWarehouseId("88"); application.setFulfillmentStatus("WAIT_PRE_DISPATCH");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(application);
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(application);
RelabelItem selected = relabelItem(701L, "CARTON-A", 100L);
@@ -266,6 +284,94 @@ class PdaPalletOperationServiceTest {
relabelPalletMutations);
}
@Test void actualSourceReconcilesOnlySelectedHoldAndKeepsOriginalInboundBalance() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
ReflectionTestUtils.setField(service, "relabelWorkHistory", histories);
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(900L); task.setStatus("COMPLETED");
when(tasks.find("100", 600L)).thenReturn(task);
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setRelabelNo("RL-1");
app.setCustomerId("100"); app.setOverseasWarehouseId("88"); app.setStatus("READY_FOR_OUTBOUND");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(app);
RelabelItem item = relabelItem(701L, "CARTON-A", 101L);
when(relabelItems.selectList(any())).thenReturn(List.of(item));
when(relabelItems.selectById(701L)).thenReturn(item);
InventoryPosition anchor = position(101L, 30L, "ACCOUNTING", 2L, 3L);
InventoryPosition actual = position(100L, 20L, "SOURCE", 0L, 4L); actual.setAvailablePackages(5L);
AtomicReference<InventoryPosition> target = new AtomicReference<>();
when(positions.selectById(anyLong())).thenAnswer(invocation -> {
long id = invocation.getArgument(0); return id == 101L ? anchor : id == 100L ? actual : target.get();
});
doAnswer(invocation -> { target.set(invocation.getArgument(0)); return 1; }).when(positions).insert(any(InventoryPosition.class));
when(mapper.lockPosition(eq("88"), anyLong())).thenAnswer(invocation -> {
InventoryPosition p = ((Long) invocation.getArgument(1)) == 101L ? anchor : actual;
PalletInventoryRow row = row(p.getId(), p.getPalletId(), p.getVersion(), p.getAvailablePackages());
row.setHoldPackages(p.getHoldPackages()); return row;
});
var src = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, null, "OCCUPIED", null, 1);
var dst = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "IDLE", null, 2);
when(physical.resolve("SOURCE")).thenReturn(src); when(physical.resolve("TARGET")).thenReturn(dst);
when(physical.apply(eq(88L), any(), anyString(), isNull(), anyString(), any()))
.thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2)));
when(mapper.content("88", 10L)).thenReturn(List.of());
when(mapper.palletPackages("88", 20L)).thenAnswer(invocation -> actual.getAvailablePackages() + actual.getHoldPackages());
when(mapper.palletPackages("88", 10L)).thenAnswer(invocation -> target.get() == null ? 0L : target.get().getHoldPackages());
InventoryBalance balance = new InventoryBalance(); balance.setId(200L); balance.setCustomerId("100");
balance.setOverseasWarehouseId("88"); balance.setInboundCode("ORIGINAL-INBOUND");
when(balances.selectById(200L)).thenReturn(balance);
AtomicReference<PdaPalletOperation> op = new AtomicReference<>();
doAnswer(invocation -> { op.set(invocation.getArgument(0)); return 1; }).when(mapper).insert(any(PdaPalletOperation.class));
when(mapper.selectById(anyLong())).thenAnswer(invocation -> op.get());
var result = service.relabelDisassemble(identity(), new PalletOperationRequests.RelabelDisassemble(
"qty-1", "RL-1", new PalletOperationRequests.PalletExpectation("SOURCE", 1),
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("CARTON-A"), "quantity",
new PalletOperationRequests.MoveLine(100L, 1L, 4L)));
assertEquals("COMPLETED", result.status());
assertEquals(1L, anchor.getHoldPackages()); // Other order's hold is not moved.
assertEquals(1L, anchor.getAvailablePackages());
assertEquals(4L, actual.getAvailablePackages()); assertEquals(0L, actual.getHoldPackages());
assertEquals(1L, target.get().getHoldPackages()); assertEquals(200L, target.get().getBalanceId());
assertEquals(7L, anchor.getAvailablePackages() + anchor.getHoldPackages() + actual.getAvailablePackages()
+ actual.getHoldPackages() + target.get().getHoldPackages());
verify(relabelPalletMutations).moveSelectedItems(eq(app), argThat(values -> values.get(100L).equals(List.of(item))), anyMap());
ArgumentCaptor<InventoryLedger> entries = ArgumentCaptor.forClass(InventoryLedger.class);
verify(ledgers, times(4)).insert(entries.capture());
assertTrue(entries.getAllValues().stream().allMatch(entry -> "ORIGINAL-INBOUND".equals(entry.getInboundCode())));
verify(balances, never()).updateById(any(InventoryBalance.class));
}
@Test void boundMarksSurviveReloadButDoNotBlockANewReworkRound() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
ReflectionTestUtils.setField(service, "relabelWorkHistory", histories);
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100");
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(900L);
when(tasks.find("100", 600L)).thenReturn(task);
var history = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
history.setDetailJson("{\"round\":\"INITIAL\",\"cartonMarks\":[\"CARTON-A\"]}");
when(histories.selectList(any())).thenReturn(List.of(history));
assertEquals(java.util.Set.of("CARTON-A"), ReflectionTestUtils.invokeMethod(service, "boundRelabelMarks", app));
task.setReviewedAt(LocalDateTime.of(2026, 9, 15, 12, 0));
assertEquals(java.util.Set.of(), ReflectionTestUtils.invokeMethod(service, "boundRelabelMarks", app));
}
@Test void createsPalletBeforeCustomerConfirmationOnlyForClaimingOperator() {
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setRelabelNo("RL-1");
app.setCustomerId("100"); app.setOverseasWarehouseId("88"); app.setStatus("PROCESSING");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setStatus("PROCESSING"); task.setOperatorKey("7");
when(tasks.find("100", 600L)).thenReturn(task);
when(physical.ensureRelabelPallet(88L, "RL-1:before-confirm")).thenReturn(pallet(30L, "NEW", 88L, 0));
assertEquals("NEW", service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "before-confirm")).palletCode());
task.setOperatorKey("other");
assertThrows(ResponseStatusException.class, () -> service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "different")));
}
private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); }
private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) {
return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,