diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBinding.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBinding.java index 87e6df524..f12cdc5dd 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBinding.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBinding.java @@ -133,8 +133,10 @@ public class AppointmentWorkDispatchBinding implements AppointmentWorkDispatchPo return new Allocation(row.palletId(),row.batchKey(),version,row.customerId(),row.sourceKey(),row.destinationKey(),row.packages()); } private void validatePhysical(String customer,PhysicalItem item,List selected,boolean received,boolean picked) { - var physical=picked?mapper.pickedPhysical(item.itemId(),customer) - :received?mapper.receivedPhysical(item.planPalletId(),customer):mapper.physical(item.planPalletId(),customer); + var physical=new ArrayList<>(picked?mapper.pickedPhysical(item.itemId(),customer) + :received?mapper.receivedPhysical(item.planPalletId(),customer):mapper.physical(item.planPalletId(),customer)); + // 扫码已从库位扣减;审核与后续扫码须合并本明细已拣逐票归属,发车仍只认真实已拣。 + if(received && !picked) physical.addAll(mapper.pickedPhysical(item.itemId(),customer)); if(physical.isEmpty()) throw conflict("托盘缺少真实逐票分配,不能按整票或整托总数猜测批次"); var canonical=AppointmentWorkPhysicalSources.canonical(physical,sources); var remaining=new HashMap(); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBindingTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBindingTest.java index 7b497e606..f651e6a98 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBindingTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBindingTest.java @@ -59,14 +59,16 @@ class AppointmentWorkDispatchBindingTest { when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null))); assertDoesNotThrow(()->service.requireDeparture("D-20")); when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4))); + when(mapper.pickedPhysical(30,"C")).thenReturn(List.of()); var departure=java.time.Instant.parse("2026-10-18T12:00:00Z"); var arrival=java.time.Instant.parse("2026-10-18T17:00:00Z"); clearInvocations(mapper); assertNull(service.reviewBlockReason("D-20",departure,arrival)); - verify(mapper,never()).pickedPhysical(anyLong(),anyString()); + verify(mapper).pickedPhysical(30,"C"); assertEquals(arrival,service.reviewEvidence("D-20").get(0).startAt(),"10点洛杉矶预约应按当地时区解释"); // 目的仓编号及时间由整车入口校验;此端口只核实真实来源分配。 assertNull(service.reviewBlockReason("D-20",departure,null)); + when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4))); when(item.packageCount()).thenReturn(4);when(mapper.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",20)).thenReturn(1L); assertTrue(service.departureBlockReason("D-20").contains("分配量")); when(item.packageCount()).thenReturn(4);when(mapper.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",20)).thenReturn(0L); @@ -85,6 +87,7 @@ class AppointmentWorkDispatchBindingTest { assertDoesNotThrow(()->service.requireDeparture("D-20")); when(receiving.currentAgreement("FBA_APPOINTMENT:1","C")).thenReturn(null); assertDoesNotThrow(()->service.requireDeparture("D-20")); + when(mapper.receivedPhysical(10,"C")).thenReturn(List.of()); assertNull(service.reviewBlockReason("D-20",departure,arrival)); verifyNoInteractions(changes); } @@ -122,6 +125,31 @@ class AppointmentWorkDispatchBindingTest { when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","CUSTOMER:1",null,4))); assertDoesNotThrow(()->service.requireDeparture("D-20"));verifyNoInteractions(appointments); } + @Test void partialPickingKeepsReviewEligibleButDepartureStillRequiresAllOriginalSources() { + var mapper=mock(AppointmentWorkDispatchMapper.class);var sources=mock(AppointmentSourceService.class); + var service=new AppointmentWorkDispatchBinding(mapper,mock(InboundFbaAppointmentService.class),sources,mock(AppointmentWorkChanges.class),mock(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore.class),mock(AppointmentWorkDispatchCandidates.class)); + when(mapper.dispatchPhysical("D")).thenReturn(List.of(new AppointmentWorkDispatchMapper.DispatchPhysical(30,10L,"C","RECEIVED",4))); + when(mapper.bindings("D")).thenReturn(List.of( + new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:1","FBA:A",1), + new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:2","FBA:A",3))); + when(sources.canonicalOrders(anyList())).thenAnswer(call->((List)call.getArgument(0)).stream() + .map(ref->new AppointmentCoverageService.SourceRef(ref.customerId(),"ORDER:"+ref.orderId(),null)).toList()); + var first=new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:A",null,1); + var second=new AppointmentWorkDispatchMapper.Physical(2L,"C","FBA:A",null,3); + when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(second)); + when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(first)); + assertNull(service.reviewBlockReason("D",null,null)); + assertNotNull(service.departureBlockReason("D")); + when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(3L,"C","FBA:A",null,1))); + assertNotNull(service.reviewBlockReason("D",null,null),"同箱数的其他来源不能补足原分配"); + when(mapper.pickedPhysical(30,"C")).thenReturn(List.of()); + assertNotNull(service.reviewBlockReason("D",null,null),"缺失真实扫码事实不能推算补齐"); + when(mapper.receivedPhysical(10,"C")).thenReturn(List.of()); + when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(first,second)); + assertNull(service.reviewBlockReason("D",null,null)); + assertNull(service.departureBlockReason("D")); + verify(mapper,atLeastOnce()).pickedPhysical(30,"C"); + } @Test void repeatedPhysicalRowsForSameOrderResolveByIdentityWithoutIndexingDistinctResults() { var sources=mock(AppointmentSourceService.class); var one=new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:A",4,4);