fix container customer snapshot schema migration
This commit is contained in:
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+3
-1
@@ -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," +
|
||||
|
||||
+12
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+10
-2
@@ -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) {}
|
||||
|
||||
+16
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+104
-19
@@ -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); }
|
||||
|
||||
+18
-7
@@ -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,
|
||||
|
||||
+14
-4
@@ -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());
|
||||
|
||||
+22
-1
@@ -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 {
|
||||
|
||||
+8
-2
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user