diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java index 93bfe6f8a..7207d64e0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java @@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -254,8 +255,10 @@ public class PdaPickSettlementService { throw conflict("入仓单缺少真实订单引用"); } List quantities = lines.stream() - .map(line -> new OrderFulfillmentSplitClient.QuantityLine( - line.item().inboundDetailId(), line.item().packageCount())) + .collect(Collectors.groupingBy(line -> line.item().inboundDetailId(), LinkedHashMap::new, + Collectors.summingInt(line -> count(line.item().packageCount(), "测量明细箱数")))) + .entrySet().stream() + .map(entry -> new OrderFulfillmentSplitClient.QuantityLine(entry.getKey(), entry.getValue())) .toList(); OrderFulfillmentSplitClient.CreateRequest request = new OrderFulfillmentSplitClient.CreateRequest( source.tenantId(), root.orderId(), source.orderId(), loadingPlanId, containerBookingId, @@ -333,15 +336,18 @@ public class PdaPickSettlementService { if (!measuredDetailIds.containsAll(pickedByDetail.keySet())) { throw conflict("PDA 已拣明细未匹配已审核测量明细"); } + Map remainingPickedByDetail = new HashMap<>(pickedByDetail); List result = new ArrayList<>(); for (MeasurementItemRow item : measurements) { int measured = count(item.packageCount(), "测量明细箱数"); - int picked = pickedByDetail.getOrDefault(item.inboundDetailId(), 0); - if (picked > measured) { - throw conflict("PDA 已拣箱数超过测量明细箱数"); - } + int requested = remainingPickedByDetail.getOrDefault(item.inboundDetailId(), 0); + int picked = Math.min(measured, requested); + remainingPickedByDetail.put(item.inboundDetailId(), requested - picked); result.add(new MeasurementSplitLine(item, picked, measured - picked)); } + if (remainingPickedByDetail.values().stream().anyMatch(remaining -> remaining != 0)) { + throw conflict("PDA 已拣箱数超过测量明细箱数"); + } return result; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index efa936bbe..e902a6e79 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -259,18 +259,16 @@ public class SmartLoadingService { if (replay != null) return replay; LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName); ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId()); - for (Candidate candidate : candidates(booking.getWarehouseId())) { - if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue; - if (!sameChannel(booking, candidate)) continue; - if (!sameCustomerScope(booking, candidate)) continue; + for (InboundCandidate candidate : inboundCandidates(plan.getContainerBookingId(), booking.getWarehouseId())) { if (StringUtils.hasText(booking.getDestinationWarehouseCode()) && !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue; - int count = fittingBoxes(booking, repository.items(planId), candidate); + int count = fittingBoxes(booking, repository.items(planId), candidate.availablePackageCount(), + candidate.averageUnitVolumeCbm(), candidate.unitGrossWeightKg()); if (count > 0) { plan = repository.plan(planId); - assign(planId, new AssignRequest(candidate.inboundId(), candidate.inboundDetailId(), - count, plan.getVersion(), "REC-" + sha256(operation + '|' + candidate.inboundId() - + '|' + candidate.inboundDetailId())), operatorId, operatorName); + assignInbound(planId, new AssignInboundRequest(candidate.inboundId(), count, + plan.getVersion(), "REC-INB-" + sha256(operation + '|' + candidate.inboundId()) + .substring(0, 48)), operatorId, operatorName); } } LoadingPlanEntity recommended = repository.plan(planId); @@ -510,14 +508,18 @@ public class SmartLoadingService { } } private int fittingBoxes(ContainerBookingEntity b,List lines,Candidate c){ + return fittingBoxes(b, lines, c.availablePackageCount(), c.unitVolumeCbm(), c.unitGrossWeightKg()); + } + private int fittingBoxes(ContainerBookingEntity b,List lines,int available, + BigDecimal unitVolume,BigDecimal unitWeight){ if(b.getCapacityCbm()==null||b.getMaxWeightKg()==null)return 0; BigDecimal usedV=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add); BigDecimal usedW=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add); BigDecimal leftV=b.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO).subtract(usedV); BigDecimal leftW=b.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO).subtract(usedW); - int byV=unit(c.unitVolumeCbm()).signum()==0?c.availablePackageCount():leftV.max(BigDecimal.ZERO).divideToIntegralValue(c.unitVolumeCbm()).intValue(); - int byW=unit(c.unitGrossWeightKg()).signum()==0?c.availablePackageCount():leftW.max(BigDecimal.ZERO).divideToIntegralValue(c.unitGrossWeightKg()).intValue(); - return Math.max(0,Math.min(c.availablePackageCount(),Math.min(byV,byW))); + int byV=unit(unitVolume).signum()==0?available:leftV.max(BigDecimal.ZERO).divideToIntegralValue(unitVolume).intValue(); + int byW=unit(unitWeight).signum()==0?available:leftW.max(BigDecimal.ZERO).divideToIntegralValue(unitWeight).intValue(); + return Math.max(0,Math.min(available,Math.min(byV,byW))); } private Plan view(LoadingPlanEntity p,ContainerBookingEntity b){ var task = loadingPickService.mutationSnapshot(p.getId()); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java index f4fd32ed0..306cbe216 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java @@ -79,6 +79,37 @@ class PdaPickSettlementServiceTest { verify(splits).convergePartialPlanItem(21L, 9L, "operator"); } + @Test + void distributesOnePickedCartonAcrossRepeatedMeasurementRowsOfTheSameInboundDetail() { + LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 1, 1, "PICKED"); + resolved(List.of(item)); + when(splits.lockParent(31L)).thenReturn(parent(2)); + when(splits.lockLatestApprovedItemsForInbound(10001L, 31L)).thenReturn(List.of( + new MeasurementItemRow(51L, 61L, 41L, 1), + new MeasurementItemRow(52L, 61L, 41L, 1))); + when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation -> + List.of(allocation(1L, 1, 501L, 2))); + when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success( + new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 1))))); + writes(1, 1); + when(splits.insertSplitItem(anyLong(), eq(10001L), anyLong(), eq(31L), anyLong(), anyInt(), + eq(41L), eq(61L), eq(52L), eq(1))).thenReturn(1); + when(splits.cloneMeasurementItem(eq(10001L), eq(61L), eq(52L), anyLong(), + anyLong(), anyLong(), anyInt(), eq(1))).thenReturn(1); + when(splits.updateMeasurementItemForLoaded(10001L, 52L, 0)).thenReturn(1); + + service.settle(1L, 9L, "operator"); + + verify(splits).updateMeasurementItemForLoaded(10001L, 51L, 1); + verify(splits).updateMeasurementItemForLoaded(10001L, 52L, 0); + verify(splits).cloneMeasurementItem(eq(10001L), eq(61L), eq(52L), anyLong(), + anyLong(), anyLong(), anyInt(), eq(1)); + verify(orderSplits).split(eq("internal-token"), eq(9L), + org.mockito.ArgumentMatchers.argThat(request -> request.lines().size() == 1 + && request.lines().get(0).sourceDetailId() == 41L + && request.lines().get(0).packageCount() == 2)); + } + @Test void reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback() { LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 3, 1, "PARTIAL");