fix container customer snapshot schema migration

This commit is contained in:
2026-08-24 13:36:38 +08:00
committed by liuyihe
parent fe4eb0b593
commit b72dd54b80
23 changed files with 368 additions and 49 deletions
@@ -17,6 +17,7 @@ public record InboundPlanListResponse(
String fbaWarehouseCode,
String customerName,
Integer packageCount,
Integer pickedPackageCount,
BigDecimal volumeCbm,
Integer palletCount,
String status,
@@ -27,4 +28,17 @@ public record InboundPlanListResponse(
String dispatchStatus,
LocalDateTime createdAt
) {
public InboundPlanListResponse(Long lineId, Long planId, String planNo, String customerId,
Long orderId, String inboundCode, String orderType,
String warehouseId, String warehouseCode, String warehouseName,
String fbaWarehouseCode, String customerName, Integer packageCount,
BigDecimal volumeCbm, Integer palletCount, String status,
String appointmentNo, String appointmentStatus,
LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt,
String dispatchStatus, LocalDateTime createdAt) {
this(lineId, planId, planNo, customerId, orderId, inboundCode, orderType, warehouseId,
warehouseCode, warehouseName, fbaWarehouseCode, customerName, packageCount, 0,
volumeCbm, palletCount, status, appointmentNo, appointmentStatus, appointmentAt,
expectedLoadingAt, dispatchStatus, createdAt);
}
}
@@ -14,10 +14,20 @@ public record InboundPlanResponse(
int lineCount,
int palletCount,
BigDecimal totalVolumeCbm,
int expectedPackageCount,
int pickedPackageCount,
List<Line> lines,
List<Pallet> pallets,
List<Allocation> allocations
) {
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
String containerNo, String status, int lineCount, int palletCount,
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
List<Allocation> allocations) {
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
totalVolumeCbm, lines.stream().mapToInt(Line::requiredPackageCount).sum(),
allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations);
}
public record Line(Long id, Long orderId, String inboundCode, String fbaWarehouseCode, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
BigDecimal volumeCbm, int requiredPackageCount, int plannedPalletCount) {
@@ -18,6 +18,7 @@ public interface InboundPlanOrderMapper {
@Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.container_no AS containerNo," +
"o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," +
"COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," +
"COALESCE(o.pallet_count,0) AS recommendedPalletCount," +
"NULLIF(o.fba_warehouse_code,'') AS fbaWarehouseCode," +
"COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,''))) AS destinationWarehouseId," +
"COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,''))) AS destinationWarehouseCode," +
@@ -30,7 +31,7 @@ public interface InboundPlanOrderMapper {
"AS destinationMismatchCount " +
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
"WHERE o.customer_id=#{customerId} AND o.id=#{orderId} " +
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.fba_warehouse_code," +
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.pallet_count,o.fba_warehouse_code," +
"o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " +
"ORDER BY o.id FOR UPDATE")
List<SourceOrderSnapshot> lockOrder(@Param("customerId") String customerId,
@@ -39,7 +39,9 @@ public interface InboundPlanQueryMapper {
"l.order_id orderId,l.inbound_code inboundCode,o.order_type orderType," +
"l.destination_warehouse_id warehouseId,l.destination_warehouse_code warehouseCode," +
"l.destination_warehouse_name warehouseName,l.fba_warehouse_code fbaWarehouseCode," +
"o.customer_name customerName,l.required_package_count packageCount,l.volume_cbm volumeCbm," +
"o.customer_name customerName,l.required_package_count packageCount," +
"COALESCE((SELECT SUM(pa.picked_package_count) FROM ll_inbound_plan_pallet_allocation pa WHERE pa.line_id=l.id),0) pickedPackageCount," +
"l.volume_cbm volumeCbm," +
"l.planned_pallet_count palletCount,p.status status,a.appointment_no appointmentNo," +
"a.appointment_status appointmentStatus,a.appointment_at appointmentAt," +
"a.expected_loading_at expectedLoadingAt," +
@@ -11,6 +11,7 @@ public record SourceOrderSnapshot(
String inboundOrderNo,
BigDecimal volumeCbm,
int packageCount,
int recommendedPalletCount,
String fbaWarehouseCode,
String destinationWarehouseId,
String destinationWarehouseCode,
@@ -18,13 +19,23 @@ public record SourceOrderSnapshot(
int destinationCount,
int destinationMismatchCount
) {
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
int packageCount, String fbaWarehouseCode, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
int destinationCount, int destinationMismatchCount) {
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
packageCount, 0, fbaWarehouseCode, destinationWarehouseId, destinationWarehouseCode,
destinationWarehouseName, destinationCount, destinationMismatchCount);
}
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
int packageCount, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
int destinationCount, int destinationMismatchCount) {
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
packageCount, null, destinationWarehouseId, destinationWarehouseCode,
packageCount, 0, null, destinationWarehouseId, destinationWarehouseCode,
destinationWarehouseName, destinationCount, destinationMismatchCount);
}
}
@@ -108,6 +108,15 @@ public class InboundPdaController {
return ApiResponse.success(service.complete(scope(user, customerId), planNo));
}
@PostMapping("/partial-complete")
public ApiResponse<PdaResponses.Task> partialComplete(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo,
@RequestParam(required = false) String customerId,
@Valid @RequestBody PdaRequests.ForceComplete request) {
requireOperate(user);
return ApiResponse.success(service.partialComplete(scope(user, customerId), planNo, request));
}
@PostMapping("/unload-photos")
public ApiResponse<PdaResponses.Task> uploadUnloadPhotos(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String planNo,
@@ -20,8 +20,16 @@ public final class PdaRequests {
public record ConfirmMerge(@Positive long sourceAllocationId, @Positive int packageCount,
@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
public record ScanCarton(@NotBlank String cartonMark, @Positive long planPalletId,
@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
public record ScanCarton(@NotBlank String cartonMark, Long planPalletId, Integer packageCount,
@NotBlank String idempotencyKey, String operatorId, String operatorName) {
/** Backward-compatible single-carton request with an explicitly selected pallet. */
public ScanCarton(String cartonMark, long planPalletId, String idempotencyKey,
String operatorId, String operatorName) {
this(cartonMark, planPalletId, 1, idempotencyKey, operatorId, operatorName);
}
}
public record ForceComplete(@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
public record RemoveCarton(@NotBlank String cartonMark, @NotBlank String idempotencyKey,
String operatorId, String operatorName) {}
@@ -10,7 +10,16 @@ public final class PdaResponses {
public record Task(long taskId, String taskNo, String planNo, String status,
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
LocalDateTime estimatedEmptyReturnAt) {}
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
int pickedPackageCount) {
public Task(long taskId, String taskNo, String planNo, String status,
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
LocalDateTime estimatedEmptyReturnAt) {
this(taskId, taskNo, planNo, status, pushedAt, startedAt, completedAt,
unloadPhotoFileIds, unloadPhotoUploadedAt, estimatedEmptyReturnAt, 0, 0);
}
}
public record UnloadPhotoFile(long fileId, String fileName, String contentType, long sizeBytes) {}
@@ -29,5 +38,10 @@ public final class PdaResponses {
boolean sourcePalletScrapped) {}
public record Carton(String result, String cartonMark, long planPalletId, long allocationId,
int palletActualPackageCount) {}
int palletActualPackageCount, String palletNo, int scannedPackageCount) {
public Carton(String result, String cartonMark, long planPalletId, long allocationId,
int palletActualPackageCount) {
this(result, cartonMark, planPalletId, allocationId, palletActualPackageCount, null, 1);
}
}
}
@@ -73,6 +73,9 @@ public class InboundPdaWorkflowService {
}
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
LocalDateTime now = LocalDateTime.now();
if (task != null && terminal(task.getStatus())) {
throw conflict("PDA inbound task is already completed");
}
if (task == null) {
task = new InboundPdaTask();
task.setId(IdWorker.getId());
@@ -91,7 +94,7 @@ public class InboundPdaWorkflowService {
plan.setStatus("PDA_PENDING");
plan.setUpdatedAt(now);
repository.update(plan);
PdaResponses.Task result = task(task, planNo);
PdaResponses.Task result = taskResponse(task, plan, planNo);
saveOperation(customerId, task.getId(), null, "PUSH", request.idempotencyKey(), request, result);
return result;
}
@@ -132,7 +135,7 @@ public class InboundPdaWorkflowService {
InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaTask task = repository.task(customerId, plan.getId());
if (task == null) throw notFound("PDA task not found");
return task(task, planNo);
return taskResponse(task, plan, planNo);
}
@Transactional
@@ -220,39 +223,62 @@ public class InboundPdaWorkflowService {
if (!("UNASSIGNED".equals(carton.getStatus()) || "EXPECTED".equals(carton.getStatus()))) {
throw conflict("Carton mark has already been scanned; remove it before scanning again");
}
InboundPlanPallet pallet = requirePallet(plan.getId(), request.planPalletId());
long requestedPalletId = request.planPalletId() == null ? 0L : request.planPalletId();
InboundPlanPallet pallet = requestedPalletId > 0
? requirePallet(plan.getId(), requestedPalletId)
: autoPallet(plan, carton);
if (isNotAssigned(pallet)) throw conflict("Planned pallet must be assigned and locked before carton scanning");
if (!sameWarehouse(carton, pallet)) throw conflict("Carton and pallet destination warehouses must match");
InboundPlanLine line = carton.getLineId() == null
? repository.lineForCarton(plan.getId(), carton)
: requireLine(plan.getId(), carton.getLineId());
if (line == null) throw notFound("Carton mark does not belong to cargo in this inbound plan");
if (repository.scannedCartons(plan.getId(), line.getId()) >= value(line.getRequiredPackageCount())) {
int requestedCount = request.packageCount() == null ? 1 : request.packageCount();
if (requestedCount < 1) throw conflict("Package count must be positive");
int alreadyScanned = (int) repository.scannedCartons(plan.getId(), line.getId());
if (alreadyScanned + requestedCount > value(line.getRequiredPackageCount())) {
throw conflict("This inbound plan line has reached its required carton count");
}
List<InboundPlanCarton> cartonsToScan = new ArrayList<>();
cartonsToScan.add(carton);
if (requestedCount > 1) {
repository.cartons(plan.getId()).stream()
.filter(candidate -> !Objects.equals(candidate.getId(), carton.getId()))
.filter(candidate -> Objects.equals(candidate.getLineId(), line.getId()))
.filter(candidate -> "UNASSIGNED".equals(candidate.getStatus()) || "EXPECTED".equals(candidate.getStatus()))
.filter(candidate -> sameWarehouse(candidate, pallet))
.limit(requestedCount - 1L).forEach(cartonsToScan::add);
if (cartonsToScan.size() != requestedCount) throw conflict("Not enough unscanned cartons for the requested quantity");
}
InboundPlanPalletAllocation allocation = repository.lockAllocation(plan.getId(), line.getId(), pallet.getId());
if (allocation == null) {
allocation = new InboundPlanPalletAllocation(); allocation.setId(IdWorker.getId()); allocation.setPlanId(plan.getId());
allocation.setLineId(line.getId()); allocation.setPalletId(pallet.getId()); allocation.setAllocatedVolumeCbm(ZERO_VOLUME);
allocation.setPlannedPackageCount(0); allocation.setCountedPackageCount(0); allocation.setPickedPackageCount(0);
allocation.setActualVolumeCbm(ZERO_VOLUME); allocation.setStatus("PICKING"); allocation.setVersion(0); repository.insert(allocation);
} else if (value(allocation.getPlannedPackageCount()) > 0
&& value(allocation.getPickedPackageCount()) + requestedCount > value(allocation.getPlannedPackageCount())) {
throw conflict("Requested carton quantity exceeds the recommended pallet package count");
}
BigDecimal perPackage = perPackageVolume(line);
carton.setOrderId(line.getOrderId()); carton.setPlanId(plan.getId()); carton.setLineId(line.getId());
carton.setPlanPalletId(pallet.getId()); carton.setWarehousePalletId(pallet.getWarehousePalletId());
carton.setWarehouseLocationId(pallet.getWarehouseLocationId()); carton.setPdaTaskId(task.getId());
carton.setStatus("ON_PALLET"); carton.setBoundAt(LocalDateTime.now()); carton.setUpdatedAt(LocalDateTime.now());
carton.setVersion(value(carton.getVersion()) + 1); repository.update(carton);
allocation.setCountedPackageCount(value(allocation.getCountedPackageCount()) + 1);
allocation.setPickedPackageCount(value(allocation.getPickedPackageCount()) + 1);
allocation.setActualVolumeCbm(volume(allocation.getActualVolumeCbm()).add(perPackage).setScale(4, RoundingMode.HALF_UP));
LocalDateTime now = LocalDateTime.now();
for (InboundPlanCarton item : cartonsToScan) {
item.setOrderId(line.getOrderId()); item.setPlanId(plan.getId()); item.setLineId(line.getId());
item.setPlanPalletId(pallet.getId()); item.setWarehousePalletId(pallet.getWarehousePalletId());
item.setWarehouseLocationId(pallet.getWarehouseLocationId()); item.setPdaTaskId(task.getId());
item.setStatus("ON_PALLET"); item.setBoundAt(now); item.setUpdatedAt(now);
item.setVersion(value(item.getVersion()) + 1); repository.update(item);
}
allocation.setCountedPackageCount(value(allocation.getCountedPackageCount()) + requestedCount);
allocation.setPickedPackageCount(value(allocation.getPickedPackageCount()) + requestedCount);
allocation.setActualVolumeCbm(volume(allocation.getActualVolumeCbm()).add(perPackage.multiply(BigDecimal.valueOf(requestedCount))).setScale(4, RoundingMode.HALF_UP));
allocation.setStatus(value(allocation.getPickedPackageCount()) >= value(allocation.getPlannedPackageCount()) ? "PICKED" : "PICKING");
bump(allocation); repository.update(allocation);
pallet.setActualPackageCount(value(pallet.getActualPackageCount()) + 1);
pallet.setActualVolumeCbm(volume(pallet.getActualVolumeCbm()).add(perPackage).setScale(4, RoundingMode.HALF_UP));
pallet.setActualPackageCount(value(pallet.getActualPackageCount()) + requestedCount);
pallet.setActualVolumeCbm(volume(pallet.getActualVolumeCbm()).add(perPackage.multiply(BigDecimal.valueOf(requestedCount))).setScale(4, RoundingMode.HALF_UP));
repository.update(pallet); startPicking(task, plan, request.operatorId(), request.operatorName());
PdaResponses.Carton result = new PdaResponses.Carton("SCANNED", carton.getCartonMark(), pallet.getId(),
allocation.getId(), pallet.getActualPackageCount());
allocation.getId(), pallet.getActualPackageCount(), pallet.getPalletNo(), requestedCount);
saveOperation(customerId, task.getId(), allocation.getId(), "SCAN_CARTON", request.idempotencyKey(), request, result);
return result;
}
@@ -378,6 +404,7 @@ public class InboundPdaWorkflowService {
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
if (task == null) throw conflict("Push the inbound plan to PDA before operating");
if ("COMPLETED".equals(task.getStatus())) return task(task, planNo);
if ("PARTIAL_INBOUND".equals(task.getStatus())) throw conflict("PDA inbound task was partially completed");
List<Long> unloadPhotoFileIds = unloadPhotoFileIds(task);
if (unloadPhotoFileIds.isEmpty() || task.getUnloadPhotoUploadedAt() == null
|| task.getEstimatedEmptyReturnAt() == null) {
@@ -414,7 +441,36 @@ public class InboundPdaWorkflowService {
plan.setUpdatedAt(now);
repository.update(plan);
unloadCompletedOutbox.publish(plan, task, unloadPhotoFileIds);
return task(task, planNo);
return taskResponse(task, plan, planNo);
}
/** Explicit operator decision to receive only the cartons actually picked. */
@Transactional
public PdaResponses.Task partialComplete(String customerId, String planNo, PdaRequests.ForceComplete request) {
InboundPlan plan = requirePlan(customerId, planNo);
InboundPdaOperation replay = replay(customerId, "PARTIAL_COMPLETE", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Task.class);
InboundPdaTask task = requireActiveTask(customerId, plan);
List<InboundPlanLine> lines = repository.lines(plan.getId());
List<InboundPlanPalletAllocation> allocations = repository.allocations(plan.getId());
int expected = lines.stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum();
int picked = allocations.stream().mapToInt(allocation -> value(allocation.getPickedPackageCount())).sum();
if (picked <= 0) throw conflict("At least one carton must be picked before partial inbound completion");
if (picked >= expected) throw conflict("All cartons are picked; use normal inbound completion");
LocalDateTime now = LocalDateTime.now();
stockReceipts.stockIn(customerId, plan, task, lines, allocations, repository.pallets(plan.getId()), now, true);
for (InboundPlanCarton carton : repository.cartons(plan.getId())) {
if ("ON_PALLET".equals(carton.getStatus())) {
carton.setStatus("AVAILABLE"); carton.setReceivedAt(now); carton.setUpdatedAt(now);
carton.setVersion(value(carton.getVersion()) + 1); repository.update(carton);
}
}
task.setStatus("PARTIAL_INBOUND"); task.setCompletedAt(now); task.setUpdatedAt(now);
task.setVersion(value(task.getVersion()) + 1); repository.update(task);
plan.setStatus("PARTIAL_INBOUND"); plan.setUpdatedAt(now); repository.update(plan);
PdaResponses.Task result = taskResponse(task, plan, planNo);
saveOperation(customerId, task.getId(), null, "PARTIAL_COMPLETE", request.idempotencyKey(), request, result);
return result;
}
@Transactional
@@ -433,7 +489,7 @@ public class InboundPdaWorkflowService {
task.setUpdatedAt(uploadedAt);
task.setVersion(value(task.getVersion()) + 1);
repository.update(task);
PdaResponses.Task result = task(task, planNo);
PdaResponses.Task result = taskResponse(task, plan, planNo);
saveOperation(customerId, task.getId(), null, "UPLOAD_UNLOAD_PHOTOS", request.idempotencyKey(), request, result);
return result;
}
@@ -597,7 +653,7 @@ public class InboundPdaWorkflowService {
private InboundPdaTask requireActiveTask(String customerId, InboundPlan plan) {
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
if (task == null) throw conflict("Push the inbound plan to PDA before operating");
if ("COMPLETED".equals(task.getStatus())) throw conflict("PDA task is already completed");
if (terminal(task.getStatus())) throw conflict("PDA task is already completed");
return task;
}
@@ -613,6 +669,24 @@ public class InboundPdaWorkflowService {
return value;
}
private InboundPlanPallet autoPallet(InboundPlan plan, InboundPlanCarton carton) {
List<InboundPlanPallet> candidates = repository.pallets(plan.getId()).stream()
.filter(pallet -> sameWarehouse(carton, pallet))
.filter(pallet -> !isNotAssigned(pallet))
.sorted(Comparator.comparingInt(pallet -> value(pallet.getSequenceNo())))
.toList();
if (candidates.isEmpty()) throw conflict("No assigned pallet exists for the carton destination warehouse");
Map<Long, Integer> plannedByPallet = new HashMap<>();
for (InboundPlanPalletAllocation allocation : repository.allocations(plan.getId())) {
plannedByPallet.merge(allocation.getPalletId(), value(allocation.getPlannedPackageCount()), Integer::sum);
}
InboundPlanPallet selected = candidates.stream()
.filter(pallet -> value(pallet.getActualPackageCount()) < plannedByPallet.getOrDefault(pallet.getId(), Integer.MAX_VALUE))
.findFirst().orElse(null);
if (selected == null) throw conflict("All assigned pallets for the carton destination warehouse are full");
return repository.lockPallet(plan.getId(), selected.getId());
}
private InboundPlanLine requireLine(Long planId, Long id) {
InboundPlanLine value = repository.line(planId, id);
if (value == null) throw notFound("Inbound plan line not found");
@@ -629,9 +703,19 @@ public class InboundPdaWorkflowService {
}
private PdaResponses.Task task(InboundPdaTask task, String planNo) {
return task(task, planNo, 0, 0);
}
private PdaResponses.Task taskResponse(InboundPdaTask task, InboundPlan plan, String planNo) {
int expected = repository.lines(plan.getId()).stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum();
int picked = repository.allocations(plan.getId()).stream().mapToInt(allocation -> value(allocation.getPickedPackageCount())).sum();
return task(task, planNo, expected, picked);
}
private PdaResponses.Task task(InboundPdaTask task, String planNo, int expected, int picked) {
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt());
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(), expected, picked);
}
private List<Long> unloadPhotoFileIds(InboundPdaTask task) {
@@ -725,6 +809,7 @@ public class InboundPdaWorkflowService {
}
private static int value(Integer value) { return value == null ? 0 : value; }
private static boolean terminal(String status) { return "COMPLETED".equals(status) || "PARTIAL_INBOUND".equals(status); }
private static BigDecimal volume(BigDecimal value) { return value == null ? ZERO_VOLUME : value; }
private static BigDecimal nonNegative(BigDecimal value) { return value.signum() < 0 ? ZERO_VOLUME : value.setScale(4, RoundingMode.HALF_UP); }
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
@@ -46,6 +46,13 @@ public class InboundStockReceiptService {
public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task,
List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations,
List<InboundPlanPallet> pallets, LocalDateTime receivedAt) {
stockIn(customerId, plan, task, lines, allocations, pallets, receivedAt, false);
}
@Transactional
public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task,
List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations,
List<InboundPlanPallet> pallets, LocalDateTime receivedAt, boolean allowPartial) {
Map<Long, LastlegOrder> orderById = orders.selectBatchIds(lines.stream()
.map(InboundPlanLine::getOrderId).collect(Collectors.toSet())).stream()
.filter(order -> customerId.equals(order.getCustomerId()))
@@ -64,8 +71,8 @@ public class InboundStockReceiptService {
Set<Long> receivedAllocationIds = existing.stream().map(InboundStockReceipt::getAllocationId)
.filter(Objects::nonNull).collect(Collectors.toSet());
if (receivedAllocationIds.equals(expectedAllocationIds)) {
activateDropship(customerId, task.getId(), orderById, existing);
completeOrders(customerId, plan, orderById, receivedAt);
if (!allowPartial) activateDropship(customerId, task.getId(), orderById, existing);
if (!allowPartial) completeOrders(customerId, plan, orderById, receivedAt);
return;
}
throw conflict("PDA stock-in receipts are incomplete; repair the task receipts before retrying");
@@ -79,11 +86,13 @@ public class InboundStockReceiptService {
.getOrDefault(line.getId(), List.of());
long pickedPackages = lineAllocations.stream()
.mapToLong(allocation -> value(allocation.getPickedPackageCount())).sum();
if (pickedPackages != value(line.getRequiredPackageCount())) {
if (pickedPackages == 0 && allowPartial) continue;
if ((!allowPartial && pickedPackages != value(line.getRequiredPackageCount()))
|| (allowPartial && pickedPackages > value(line.getRequiredPackageCount()))) {
throw conflict("PDA picked packages must equal the inbound order package total");
}
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
? List.of() : skuRemainders(order.getId(), pickedPackages);
? List.of() : skuRemainders(order.getId(), value(line.getRequiredPackageCount()));
for (InboundPlanPalletAllocation allocation : lineAllocations) {
InboundPlanPallet pallet = requirePallet(palletById, allocation);
if ("ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
@@ -96,12 +105,14 @@ public class InboundStockReceiptService {
}
}
}
if (skuRemainders.stream().anyMatch(value -> value.remaining > 0)) {
if (!allowPartial && skuRemainders.stream().anyMatch(value -> value.remaining > 0)) {
throw conflict("Inbound SKU package snapshots were not fully received");
}
}
activateDropship(customerId, task.getId(), orderById, created);
completeOrders(customerId, plan, orderById, receivedAt);
if (!allowPartial) {
activateDropship(customerId, task.getId(), orderById, created);
completeOrders(customerId, plan, orderById, receivedAt);
}
}
private void completeOrders(String customerId, InboundPlan plan, Map<Long, LastlegOrder> orderById,
@@ -57,6 +57,11 @@ public class InboundPlanService {
@Transactional
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
return convert(customerId, selectedOrderId, List.of());
}
@Transactional
public InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos) {
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);
if (orders == null || orders.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound order not found");
@@ -88,13 +93,14 @@ public class InboundPlanService {
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
PalletAllocator.Result allocation = PalletAllocator.allocate(orders.stream()
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
order.packageCount()))
order.packageCount(), order.recommendedPalletCount()))
.toList());
plan.setPalletCount(allocation.pallets().size());
planMapper.insert(plan);
lineByOrder.values().forEach(lineMapper::insert);
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation, orderById);
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation, orderById,
sourcePalletNos == null ? List.of() : sourcePalletNos);
createAllocations(plan, allocation, lineByOrder, persistedPallets);
Map<Long, Integer> palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
@@ -175,7 +181,8 @@ public class InboundPlanService {
}
private Map<PalletAllocator.PalletDraft, InboundPlanPallet> createPallets(
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById) {
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById,
List<String> sourcePalletNos) {
Map<String, SourceOrderSnapshot> warehouseSamples = orderById.values().stream()
.collect(Collectors.toMap(this::warehouseKey, value -> value, (left, right) -> left));
Map<PalletAllocator.PalletDraft, InboundPlanPallet> result = new HashMap<>();
@@ -185,7 +192,10 @@ public class InboundPlanService {
pallet.setId(IdWorker.getId());
pallet.setCustomerId(plan.getCustomerId());
pallet.setPlanId(plan.getId());
pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", result.size() + 1));
int palletIndex = result.size();
String sourcePalletNo = palletIndex < sourcePalletNos.size() ? sourcePalletNos.get(palletIndex) : null;
pallet.setPalletNo(sourcePalletNo == null || sourcePalletNo.isBlank()
? plan.getPlanNo() + "-P" + String.format("%03d", palletIndex + 1) : sourcePalletNo.trim());
pallet.setDestinationWarehouseId(sample.destinationWarehouseId());
pallet.setDestinationWarehouseCode(sample.destinationWarehouseCode());
pallet.setDestinationWarehouseName(sample.destinationWarehouseName());
@@ -34,6 +34,23 @@ public final class PalletAllocator {
PalletDraft current = null;
int sequence = 0;
for (LineInput line : group) {
if (line.recommendedPalletCount() > 0) {
int minimumByVolume = line.volumeCbm().divide(PALLET_CAPACITY_CBM, 0, RoundingMode.CEILING).intValue();
int count = Math.max(line.recommendedPalletCount(), minimumByVolume);
BigDecimal remaining = line.volumeCbm();
for (int index = 0; index < count; index++) {
PalletDraft recommended = new PalletDraft(line.warehouseKey(), ++sequence, BigDecimal.ZERO);
pallets.add(recommended);
int remainingPallets = count - index;
BigDecimal allocated = remainingPallets == 1 ? remaining
: remaining.divide(BigDecimal.valueOf(remainingPallets), 4, RoundingMode.HALF_UP);
recommended.add(allocated);
allocations.add(new AllocationDraft(line.orderId(), recommended, allocated, 0));
remaining = remaining.subtract(allocated);
}
current = null;
continue;
}
BigDecimal remaining = line.volumeCbm();
while (remaining.signum() > 0) {
if (current == null || current.remainingCapacity().signum() == 0) {
@@ -85,7 +102,11 @@ public final class PalletAllocator {
return List.copyOf(result);
}
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount,
int recommendedPalletCount) {
public LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
this(orderId, warehouseKey, volumeCbm, packageCount, 0);
}
}
public static final class PalletDraft {
@@ -156,7 +156,9 @@ public class LoadingCompletedConsumer {
order.setOverseasWarehouseId(payload.warehouseId()); order.setOverseasWarehouseName(payload.warehouseName());
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo());
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
order.setPackageCount(source.packageCount()); order.setPalletCount(dropshipPallets.size());
order.setPackageCount(source.packageCount());
order.setPalletCount("ONE_CLICK_DROPSHIP".equals(orderType)
? dropshipPallets.size() : Objects.requireNonNullElse(source.palletCount(), 0));
order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm());
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
@@ -192,7 +194,11 @@ public class LoadingCompletedConsumer {
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history);
if ("FIRST_LEG".equals(orderType)) {
InboundPlanResponse plan = inboundPlans.convert(customerId, order.getId());
List<String> sourcePalletNos = source.pallets() == null ? List.of() : source.pallets().stream()
.map(FirstLegLoadingPlanCompletedV1.PalletSnapshot::palletNo).filter(Objects::nonNull).toList();
InboundPlanResponse plan = sourcePalletNos.isEmpty()
? inboundPlans.convert(customerId, order.getId())
: inboundPlans.convert(customerId, order.getId(), sourcePalletNos);
registerInTransit(event, payload, source, order, plan.planNo());
}
if ("ONE_CLICK_DROPSHIP".equals(orderType)) {