This commit is contained in:
2026-10-20 14:22:31 +08:00
parent 19745e46c1
commit a870b920fe
95 changed files with 2616 additions and 275 deletions
@@ -597,12 +597,19 @@ public class DeliveryTransferService {
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
throw conflict("换标单尚未完成客户确认或已不可出库");
}
if (relabelSnapshotMarks(item, selectedPosition.getId(), application.getRelabelNo()).isEmpty())
throw conflict("换标出库指定位置或箱唛快照不完整,请核对原申请");
List<RelabelItem> sourceItems = relabelItems.selectList(new LambdaQueryWrapper<RelabelItem>()
.eq(RelabelItem::getApplicationId, application.getId())
.eq(RelabelItem::getCustomerId, customerId)
.eq(RelabelItem::getSourceInventoryPositionId, request.inventoryPositionId()));
long sourcePackages = sourceItems.stream().mapToLong(source -> value(source.getNotifiedPackageCount())).sum();
if (sourcePackages != request.packages()) throw conflict("换标出库来源库存快照与任务箱数不一致");
List<String> sourceMarks = sourceItems.stream().map(RelabelItem::getCartonMark)
.filter(StringUtils::hasText).distinct().sorted().toList();
Set<String> sourceIds = sourceItems.stream().map(source -> String.valueOf(source.getId())).collect(Collectors.toSet());
if (sourcePackages != request.packages() || !sourceMarks.equals(expectedMarks)
|| !sourceIds.equals(relabelSnapshotItemIds(item)))
throw conflict("换标出库来源库存快照与任务箱数或箱唛不一致");
int releaseIndex = 0;
for (RelabelItem source : sourceItems) {
inventoryCommands.release(customerId, operatorId, new InventoryCommandRequests.Move(
@@ -720,6 +727,7 @@ public class DeliveryTransferService {
return taskResult(outbound, task);
}
/** 扫描整托前核平全部指定来源,任一不足时不执行部分拣货。 */
@Transactional
public DeliveryResponses.PdaTask pdaPickPallet(String warehouseId, String operatorId, String taskNo,
String palletNo, DeliveryRequests.PdaPalletPick request) {
@@ -769,7 +777,7 @@ public class DeliveryTransferService {
long remaining = value(item.getPackageCount()) - value(item.getPickedPackageCount());
if (remaining <= 0) continue;
Map<Long, List<DeliveryResponses.PdaPickPosition>> positionsById = pdaPickPositions(
task.getCustomerId(), boundWarehouseId, item).stream().collect(Collectors.groupingBy(
task.getCustomerId(), boundWarehouseId, item, outbound.getOutboundType()).stream().collect(Collectors.groupingBy(
DeliveryResponses.PdaPickPosition::inventoryPositionId, LinkedHashMap::new, Collectors.toList()));
for (Map.Entry<Long, List<DeliveryResponses.PdaPickPosition>> entry : positionsById.entrySet()) {
if (remaining <= 0) break;
@@ -1049,6 +1057,7 @@ public class DeliveryTransferService {
.map(task -> taskResult(requireOutboundInWarehouse(task, boundWarehouseId), task)).toList();
}
/** PDA明细展示与整托拣货使用相同的客户、仓库及来源投影。 */
public DeliveryResponses.PdaTaskDetail pdaTaskDetail(String warehouseId, String taskNo) {
String boundWarehouseId = required(warehouseId, "PDA warehouse binding is required");
OutboundPdaTask task = pdaTasks.findByNoAndWarehouse(required(taskNo, "PDA task number is required"),
@@ -1061,7 +1070,7 @@ public class DeliveryTransferService {
value(item.getPickedPackageCount()), item.getStatus(),
("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) ? returnPickPositions(outbound, task, item)
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item), item.getReturnSkuQuantity())).toList();
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item, outbound.getOutboundType()), item.getReturnSkuQuantity())).toList();
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
}
@@ -1119,13 +1128,28 @@ public class DeliveryTransferService {
&& value(item.getPickedPackageCount()) == 0 && "OUTBOUND_PENDING".equals(carton.getStatus()) ? 1 : 0, value(item.getPickedPackageCount())));
}
/** 换标使用指定实物箱及仍归本单的冻结库存;普通出库继续按可用库存和SKU匹配。 */
private List<DeliveryResponses.PdaPickPosition> pdaPickPositions(String customerId, String warehouseId,
OutboundPalletItem item) {
OutboundPalletItem item, String outboundType) {
if (item.getWarehousePalletId() == null) return List.of();
List<DeliveryResponses.PdaPickPosition> result = new ArrayList<>();
Set<String> frozenMarks = new HashSet<>(Arrays.asList(
Optional.ofNullable(item.getCartonMarks()).orElse("").split(",")));
for (InventoryPosition position : inventoryPositions.listPallet(customerId, item.getWarehousePalletId())) {
if ("RELABEL_OUTBOUND".equals(outboundType)) {
List<String> marks = relabelSnapshotMarks(item, position.getId(), null);
InventoryBalance balance = inventoryBalances.selectById(position.getBalanceId());
if (marks.isEmpty() || !customerId.equals(position.getCustomerId())
|| !warehouseId.equals(position.getOverseasWarehouseId())
|| !Objects.equals(item.getWarehousePalletId(), position.getPalletId())
|| balance == null || !customerId.equals(balance.getCustomerId())
|| !warehouseId.equals(balance.getOverseasWarehouseId())
|| !Objects.equals(item.getInboundCode(), balance.getInboundCode())
|| value(position.getHoldPackages()) < marks.size()) continue;
// 本单冻结箱先在写路径校验原申请和来源任务,再释放、分配、拣货、出库;不把其他可用箱混入。
for (String mark : marks) result.add(pdaPosition(position, item, balance, mark, 1, 0));
continue;
}
long available = value(position.getAvailablePackages());
if (available <= 0 || !warehouseId.equals(position.getOverseasWarehouseId())) continue;
InventoryBalance balance = inventoryBalances.selectById(position.getBalanceId());
@@ -1147,6 +1171,37 @@ public class DeliveryTransferService {
return result;
}
/** 仅接受完整的换标出库快照;空SKU不是通配符,必须精确匹配返工后冻结的位置和全部箱唛。 */
private List<String> relabelSnapshotMarks(OutboundPalletItem item, Long positionId, String expectedRelabelNo) {
try {
var refs = objectMapper.readTree(item.getOrderRefs());
if (!"RELABEL_ORDER".equals(refs.path("sourceType").asText())
|| !StringUtils.hasText(refs.path("relabelNo").asText())
|| expectedRelabelNo != null && !expectedRelabelNo.equals(refs.path("relabelNo").asText())
|| positionId == null || !String.valueOf(positionId).equals(refs.path("sourceInventoryPositionId").asText())
|| !refs.path("items").isArray()) return List.of();
var marks = new TreeSet<String>(); var ids = new HashSet<String>(); long packages = 0;
for (var line : refs.path("items")) {
String mark = line.path("cartonMark").asText().trim(), id = line.path("itemId").asText();
long count = line.path("packages").asLong();
if (!StringUtils.hasText(mark) || !StringUtils.hasText(id) || !ids.add(id) || !marks.add(mark) || count != 1) return List.of();
packages += count;
}
var frozen = Arrays.stream(Objects.toString(item.getCartonMarks(), "").split(","))
.map(String::trim).filter(StringUtils::hasText).collect(Collectors.toCollection(TreeSet::new));
return packages == value(item.getPackageCount()) && marks.equals(frozen) ? List.copyOf(marks) : List.of();
} catch (Exception ignored) { return List.of(); }
}
/** 释放冻结前核对快照任务主键与本申请当前位置的真实任务集合完全相同。 */
private Set<String> relabelSnapshotItemIds(OutboundPalletItem item) {
try {
var ids = new HashSet<String>();
for (var line : objectMapper.readTree(item.getOrderRefs()).path("items")) ids.add(line.path("itemId").asText());
return ids;
} catch (Exception ignored) { return Set.of(); }
}
private DeliveryResponses.PdaPickPosition pdaPosition(InventoryPosition position, OutboundPalletItem item,
InventoryBalance balance, String cartonMark,
long available, long picked) {