优化
This commit is contained in:
-8
@@ -112,14 +112,6 @@ public class ReceivingContainerController {
|
||||
return ApiResponse.success(service.resolveCarton(identity.operatorKey(), request));
|
||||
}
|
||||
|
||||
@PostMapping("/current/skus/resolve")
|
||||
public ApiResponse<ReceivingContainerResponses.SkuResolved> resolveSku(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReceivingContainerRequests.SkuResolve request) {
|
||||
PdaWarehouseIdentity identity = currentIdentity(user);
|
||||
return ApiResponse.success(service.resolveSku(identity.operatorKey(), request));
|
||||
}
|
||||
|
||||
@PostMapping("/current/pallets/resolve")
|
||||
public ApiResponse<ReceivingContainerResponses.PalletResolved> resolvePallet(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+1
-3
@@ -18,10 +18,8 @@ public final class ReceivingContainerRequests {
|
||||
public record SignedPhoto(@NotNull ReceivingPhotoRole role,
|
||||
@NotBlank @Pattern(regexp = "^[1-9][0-9]{0,18}$") String fileId) { }
|
||||
public record CartonResolve(@NotBlank String cartonMark) { }
|
||||
public record SkuResolve(@NotBlank @Size(max = 128) String skuCode) { }
|
||||
public record PalletResolve(@NotBlank String palletCode, @NotBlank String inboundCode) { }
|
||||
public record BulkReceive(@NotBlank String inboundCode, @NotBlank @Size(max = 128) String skuCode,
|
||||
@NotBlank String palletCode,
|
||||
public record BulkReceive(@NotBlank String inboundCode, @NotBlank String palletCode,
|
||||
@Positive int packageCount, @NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
public record CartonReceive(@NotBlank String cartonMark, @NotBlank String palletCode,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
+1
-8
@@ -28,20 +28,13 @@ public final class ReceivingContainerResponses {
|
||||
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
|
||||
int receivedPackages, int pendingPackages) { }
|
||||
|
||||
public record CartonResolved(String cartonMark, String skuCode, String inboundCode,
|
||||
public record CartonResolved(String cartonMark, String inboundCode,
|
||||
String destinationType, String overseasWarehouseId,
|
||||
String overseasWarehouseName, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
|
||||
int receivedPackages, int pendingPackages) { }
|
||||
|
||||
public record SkuResolved(String skuCode, String productName, String inboundCode,
|
||||
String destinationType, String overseasWarehouseId,
|
||||
String overseasWarehouseName, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
|
||||
int receivedPackages, int pendingPackages) { }
|
||||
|
||||
public record PalletResolved(String palletCode, long warehouseId, String status,
|
||||
String fbaWarehouseCode, String inboundCode,
|
||||
String destinationType, boolean compatible) { }
|
||||
|
||||
+111
-93
@@ -226,42 +226,16 @@ public class ReceivingContainerService {
|
||||
ReceivingContainerRequests.CartonResolve request) {
|
||||
ReceivingContainerSession session = requireSigned(operatorKey);
|
||||
String customerId = session.getCustomerId();
|
||||
List<InboundRef> currentRefs = refs(customerId, session.getContainerNo());
|
||||
if (currentRefs.stream().noneMatch(value ->
|
||||
"OVERSEAS".equals(destinationType(value.line(), value.order())))) {
|
||||
throw conflict("FBA destination must use bulk count");
|
||||
}
|
||||
InboundPlanCarton carton = cartons.selectOne(new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getCustomerId, customerId).eq(InboundPlanCarton::getCartonMark, request.cartonMark()));
|
||||
if (carton == null) throw notFound("Carton mark was not found");
|
||||
InboundRef ref = currentRefs.stream()
|
||||
.filter(value -> Objects.equals(value.line().getOrderId(), carton.getOrderId())).findFirst()
|
||||
.orElseThrow(() -> conflict("Carton mark belongs to another container"));
|
||||
if (!"OVERSEAS".equals(destinationType(ref.line(), ref.order()))) {
|
||||
throw conflict("FBA inbound must use bulk count instead of carton scan");
|
||||
}
|
||||
CartonRef carton = requireCartonRef(customerId, session.getContainerNo(), request.cartonMark());
|
||||
InboundRef ref = carton.ref();
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(ref);
|
||||
return new ReceivingContainerResponses.CartonResolved(carton.getCartonMark(), carton.getSkuCode(),
|
||||
return new ReceivingContainerResponses.CartonResolved(carton.carton().getCartonMark(),
|
||||
inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(),
|
||||
inbound.overseasWarehouseName(), inbound.destinationWarehouseId(),
|
||||
inbound.destinationWarehouseCode(), inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(),
|
||||
inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages());
|
||||
}
|
||||
|
||||
public ReceivingContainerResponses.SkuResolved resolveSku(String operatorKey,
|
||||
ReceivingContainerRequests.SkuResolve request) {
|
||||
ReceivingContainerSession session = requireSigned(operatorKey);
|
||||
String customerId = session.getCustomerId();
|
||||
List<SkuRef> matches = refs(customerId, session.getContainerNo()).stream()
|
||||
.filter(ref -> "FBA".equals(destinationType(ref.line(), ref.order())))
|
||||
.map(ref -> skuRef(ref, request.skuCode()))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (matches.isEmpty()) throw notFound("SKU was not found in the current FBA container");
|
||||
if (matches.size() != 1) throw conflict("SKU belongs to multiple inbound codes in the current container");
|
||||
return skuResponse(matches.get(0));
|
||||
}
|
||||
|
||||
public ReceivingContainerResponses.PalletResolved resolvePallet(String operatorKey,
|
||||
ReceivingContainerRequests.PalletResolve request) {
|
||||
ReceivingContainerSession session = requireSigned(operatorKey);
|
||||
@@ -284,22 +258,27 @@ public class ReceivingContainerService {
|
||||
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) {
|
||||
throw conflict("OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden");
|
||||
}
|
||||
SkuRef sku = requireSku(ref, request.skuCode());
|
||||
validateSkuPending(customerId, sku, request.packageCount());
|
||||
workflow.validateBulkResolved(customerId, ref.plan(), ref.line(), request.packageCount());
|
||||
List<InboundRef> fbaPlanRefs = requireFbaPlanRefs(customerId, session.getContainerNo(), ref);
|
||||
List<FbaSkuAllocation> skuAllocations = allocateFbaSkuReceipts(customerId, fbaPlanRefs, request.packageCount());
|
||||
skuAllocations.forEach(allocation -> workflow.validateBulkResolved(customerId, allocation.ref().plan(),
|
||||
allocation.ref().line(), allocation.packages()));
|
||||
WarehouseResourceGateway.WarehousePalletReference resource = warehouse.resolvePallet(request.palletCode());
|
||||
validatePallet(ref, resource);
|
||||
InboundPlanPallet pallet = choosePallet(ref, resource);
|
||||
Long resourceLockId = workflow.assignResolvedPallet(customerId, ref.plan(), pallet, resource);
|
||||
try {
|
||||
int accepted = workflow.receiveBulkResolved(customerId, ref.plan(), ref.line(), pallet,
|
||||
request.packageCount(), operatorKey, operatorName);
|
||||
recordSkuReceipt(session, sku, pallet, accepted);
|
||||
int accepted = 0;
|
||||
for (FbaSkuAllocation allocation : skuAllocations) {
|
||||
accepted += workflow.receiveBulkResolved(customerId, allocation.ref().plan(), allocation.ref().line(), pallet,
|
||||
allocation.packages(), operatorKey, operatorName);
|
||||
recordSkuReceipt(session, allocation, pallet);
|
||||
}
|
||||
markReceiving(session);
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(ref);
|
||||
int received = fbaPlanRefs.stream().mapToInt(this::receivedLinePackages).sum();
|
||||
int total = fbaPlanRefs.stream().mapToInt(refValue -> value(refValue.line().getRequiredPackageCount())).sum();
|
||||
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
|
||||
session.getContainerNo(), inbound.inboundCode(), inbound.destinationType(), resource.palletNo(), accepted,
|
||||
inbound.receivedPackages(), inbound.pendingPackages(), session.getStatus());
|
||||
session.getContainerNo(), ref.line().getInboundCode(), "FBA", resource.palletNo(), accepted,
|
||||
received, Math.max(0, total - received), session.getStatus());
|
||||
save(session, "BULK_RECEIVE", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
} catch (RuntimeException failure) {
|
||||
@@ -320,12 +299,8 @@ public class ReceivingContainerService {
|
||||
"OVERSEAS".equals(destinationType(value.line(), value.order())))) {
|
||||
throw conflict("FBA destination must use bulk count");
|
||||
}
|
||||
InboundPlanCarton carton = cartons.selectOne(new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getCustomerId, customerId).eq(InboundPlanCarton::getCartonMark, request.cartonMark()));
|
||||
if (carton == null) throw notFound("Carton mark was not found");
|
||||
InboundRef ref = currentRefs.stream()
|
||||
.filter(value -> Objects.equals(value.line().getOrderId(), carton.getOrderId())).findFirst()
|
||||
.orElseThrow(() -> conflict("Carton mark belongs to another container"));
|
||||
CartonRef carton = requireCartonRef(customerId, session.getContainerNo(), request.cartonMark());
|
||||
InboundRef ref = carton.ref();
|
||||
if (!"OVERSEAS".equals(destinationType(ref.line(), ref.order()))) {
|
||||
throw conflict("FBA destination must use bulk count");
|
||||
}
|
||||
@@ -426,7 +401,8 @@ public class ReceivingContainerService {
|
||||
List<InboundRef> result = new ArrayList<>();
|
||||
for (InboundPlan plan : plansForContainer(customerId, containerNo)) {
|
||||
for (InboundPlanLine line : lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getPlanId, plan.getId()))) {
|
||||
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getPlanId, plan.getId())
|
||||
.orderByAsc(InboundPlanLine::getId))) {
|
||||
LastlegOrder order = orders.selectById(line.getOrderId());
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Inbound line order is missing");
|
||||
result.add(new InboundRef(plan, line, order));
|
||||
@@ -439,8 +415,37 @@ public class ReceivingContainerService {
|
||||
List<InboundRef> matches = refs(customerId, containerNo).stream()
|
||||
.filter(value -> inboundCode.equals(value.line().getInboundCode())).toList();
|
||||
if (matches.isEmpty()) throw notFound("Inbound code is not part of the current container");
|
||||
if (matches.size() != 1) throw conflict("Inbound code is duplicated inside the current container");
|
||||
return matches.get(0);
|
||||
Set<Long> planIds = matches.stream().map(value -> value.plan().getId()).collect(Collectors.toSet());
|
||||
if (planIds.size() != 1) throw conflict("Inbound code is duplicated inside the current container");
|
||||
return matches.stream().min(Comparator.comparing(value -> value.line().getId())).orElseThrow();
|
||||
}
|
||||
|
||||
private CartonRef requireCartonRef(String customerId, String containerNo, String cartonMark) {
|
||||
InboundPlanCarton carton = cartons.selectOne(new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getCustomerId, customerId).eq(InboundPlanCarton::getCartonMark, cartonMark));
|
||||
if (carton == null) throw notFound("Carton mark was not found");
|
||||
List<InboundRef> matches = refs(customerId, containerNo).stream()
|
||||
.filter(value -> carton.getLineId() == null || Objects.equals(value.line().getId(), carton.getLineId()))
|
||||
.filter(value -> carton.getPlanId() == null || Objects.equals(value.plan().getId(), carton.getPlanId()))
|
||||
.filter(value -> Objects.equals(value.line().getOrderId(), carton.getOrderId())).toList();
|
||||
if (matches.size() != 1) throw conflict("Carton mark belongs to another container");
|
||||
return new CartonRef(carton, matches.get(0));
|
||||
}
|
||||
|
||||
private List<InboundRef> requireFbaPlanRefs(String customerId, String containerNo, InboundRef selected) {
|
||||
List<InboundRef> values = refs(customerId, containerNo).stream()
|
||||
.filter(value -> Objects.equals(value.plan().getId(), selected.plan().getId()))
|
||||
.sorted(Comparator.comparing(value -> value.line().getId())).toList();
|
||||
if (values.isEmpty() || values.stream().anyMatch(value ->
|
||||
!"FBA".equals(destinationType(value.line(), value.order())))) {
|
||||
throw conflict("FBA bulk receiving requires every line in the selected inbound plan to be FBA");
|
||||
}
|
||||
Set<String> destinations = values.stream().map(value -> trim(value.line().getFbaWarehouseCode()))
|
||||
.collect(Collectors.toSet());
|
||||
if (destinations.size() != 1 || destinations.contains(null)) {
|
||||
throw conflict("FBA bulk receiving requires one destination warehouse per inbound plan");
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private InboundPlanPallet choosePallet(InboundRef ref, WarehouseResourceGateway.WarehousePalletReference resource) {
|
||||
@@ -553,44 +558,54 @@ public class ReceivingContainerService {
|
||||
throw conflict("Unload photo file ID must be a positive decimal string");
|
||||
}
|
||||
|
||||
private SkuRef requireSku(InboundRef ref, String skuCode) {
|
||||
SkuRef result = skuRef(ref, skuCode);
|
||||
if (result == null) throw notFound("SKU is not part of the selected inbound code");
|
||||
private List<FbaSkuAllocation> allocateFbaSkuReceipts(String customerId, List<InboundRef> refs, int packageCount) {
|
||||
int remaining = packageCount;
|
||||
List<FbaSkuAllocation> result = new ArrayList<>();
|
||||
for (InboundRef ref : refs) {
|
||||
int linePending = value(ref.line().getRequiredPackageCount()) - receivedLinePackages(ref);
|
||||
List<SkuExpectation> expectations = skuExpectations(ref);
|
||||
int skuPending = expectations.stream().mapToInt(expectation -> expectation.expectedPackages()
|
||||
- receivedSkuPackages(customerId, ref.line().getId(), expectation.skuCode())).sum();
|
||||
if (linePending < 0 || skuPending != linePending) {
|
||||
throw conflict("FBA SKU receipt progress must exactly match the inbound line progress");
|
||||
}
|
||||
for (SkuExpectation expectation : expectations) {
|
||||
if (remaining == 0) break;
|
||||
int skuPendingPackages = expectation.expectedPackages()
|
||||
- receivedSkuPackages(customerId, ref.line().getId(), expectation.skuCode());
|
||||
int accepted = Math.min(remaining, skuPendingPackages);
|
||||
if (accepted > 0) {
|
||||
result.add(new FbaSkuAllocation(ref, expectation.skuCode(), expectation.productName(), accepted));
|
||||
remaining -= accepted;
|
||||
}
|
||||
}
|
||||
if (remaining == 0) break;
|
||||
}
|
||||
if (remaining != 0) throw conflict("Received package count exceeds the FBA inbound pending total");
|
||||
return result;
|
||||
}
|
||||
|
||||
private SkuRef skuRef(InboundRef ref, String skuCode) {
|
||||
String requested = trim(skuCode);
|
||||
if (requested == null) return null;
|
||||
private List<SkuExpectation> skuExpectations(InboundRef ref) {
|
||||
List<LastlegOrderDetail> details = orderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, ref.order().getCustomerId())
|
||||
.eq(LastlegOrderDetail::getOrderId, ref.order().getId()).orderByAsc(LastlegOrderDetail::getId));
|
||||
List<LastlegOrderDetail> matched = details.stream()
|
||||
.filter(detail -> requested.equalsIgnoreCase(trim(detail.getSkuCode()))).toList();
|
||||
int expected = matched.stream().mapToInt(detail -> value(detail.getPackageCount())).sum();
|
||||
if (expected <= 0) return null;
|
||||
String canonicalSku = matched.stream().map(LastlegOrderDetail::getSkuCode).map(ReceivingContainerService::trim)
|
||||
.filter(Objects::nonNull).findFirst().orElse(requested);
|
||||
String productName = matched.stream().map(detail -> first(detail.getProductNameZh(), detail.getProductNameEn()))
|
||||
.filter(Objects::nonNull).findFirst().orElse(null);
|
||||
return new SkuRef(ref, canonicalSku, productName, expected);
|
||||
}
|
||||
|
||||
private ReceivingContainerResponses.SkuResolved skuResponse(SkuRef sku) {
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(sku.ref());
|
||||
int received = receivedSkuPackages(sku.ref().order().getCustomerId(), sku.ref().line().getId(), sku.skuCode());
|
||||
return new ReceivingContainerResponses.SkuResolved(sku.skuCode(), sku.productName(), inbound.inboundCode(),
|
||||
inbound.destinationType(), inbound.overseasWarehouseId(), inbound.overseasWarehouseName(),
|
||||
inbound.destinationWarehouseId(), inbound.destinationWarehouseCode(),
|
||||
inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(), inbound.plannedPalletCount(),
|
||||
sku.expectedPackages(), received, Math.max(0, sku.expectedPackages() - received));
|
||||
}
|
||||
|
||||
private void validateSkuPending(String customerId, SkuRef sku, int packageCount) {
|
||||
int received = receivedSkuPackages(customerId, sku.ref().line().getId(), sku.skuCode());
|
||||
if (packageCount <= 0 || packageCount > sku.expectedPackages() - received) {
|
||||
throw conflict("Received package count exceeds the SKU pending total");
|
||||
Map<String, SkuExpectation> grouped = new LinkedHashMap<>();
|
||||
for (LastlegOrderDetail detail : details) {
|
||||
String skuCode = trim(detail.getSkuCode());
|
||||
int expected = value(detail.getPackageCount());
|
||||
if (skuCode == null || expected <= 0) continue;
|
||||
String key = skuCode.toUpperCase(java.util.Locale.ROOT);
|
||||
SkuExpectation existing = grouped.get(key);
|
||||
grouped.put(key, existing == null ? new SkuExpectation(skuCode,
|
||||
first(detail.getProductNameZh(), detail.getProductNameEn()), expected)
|
||||
: new SkuExpectation(existing.skuCode(), existing.productName(), existing.expectedPackages() + expected));
|
||||
}
|
||||
List<SkuExpectation> result = List.copyOf(grouped.values());
|
||||
int expected = result.stream().mapToInt(SkuExpectation::expectedPackages).sum();
|
||||
if (result.isEmpty() || expected != value(ref.line().getRequiredPackageCount())) {
|
||||
throw conflict("Frozen FBA SKU package snapshots must exactly match the inbound line total");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int receivedSkuPackages(String customerId, Long lineId, String skuCode) {
|
||||
@@ -598,7 +613,15 @@ public class ReceivingContainerService {
|
||||
.mapToInt(value -> value(value.getReceivedPackages())).sum();
|
||||
}
|
||||
|
||||
private void recordSkuReceipt(ReceivingContainerSession session, SkuRef sku, InboundPlanPallet pallet, int packages) {
|
||||
private int receivedLinePackages(InboundRef ref) {
|
||||
return allocations.selectList(new LambdaQueryWrapper<InboundPlanPalletAllocation>()
|
||||
.eq(InboundPlanPalletAllocation::getCustomerId, ref.order().getCustomerId())
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, ref.plan().getId())
|
||||
.eq(InboundPlanPalletAllocation::getLineId, ref.line().getId())).stream()
|
||||
.mapToInt(value -> value(value.getPickedPackageCount())).sum();
|
||||
}
|
||||
|
||||
private void recordSkuReceipt(ReceivingContainerSession session, FbaSkuAllocation sku, InboundPlanPallet pallet) {
|
||||
List<ReceivingSkuReceipt> existing = skuReceipts.lockLineSku(session.getCustomerId(),
|
||||
sku.ref().line().getId(), sku.skuCode());
|
||||
ReceivingSkuReceipt receipt = existing.stream()
|
||||
@@ -610,11 +633,11 @@ public class ReceivingContainerService {
|
||||
receipt.setPlanId(sku.ref().plan().getId()); receipt.setLineId(sku.ref().line().getId());
|
||||
receipt.setPalletId(pallet.getId()); receipt.setOrderId(sku.ref().order().getId());
|
||||
receipt.setInboundCode(sku.ref().line().getInboundCode()); receipt.setSkuCode(sku.skuCode());
|
||||
receipt.setProductName(sku.productName()); receipt.setReceivedPackages(packages);
|
||||
receipt.setProductName(sku.productName()); receipt.setReceivedPackages(sku.packages());
|
||||
receipt.setCreatedAt(now); receipt.setUpdatedAt(now); receipt.setVersion(0);
|
||||
skuReceipts.insert(receipt);
|
||||
} else {
|
||||
receipt.setReceivedPackages(value(receipt.getReceivedPackages()) + packages);
|
||||
receipt.setReceivedPackages(value(receipt.getReceivedPackages()) + sku.packages());
|
||||
receipt.setUpdatedAt(now);
|
||||
if (skuReceipts.updateById(receipt) != 1) throw conflict("SKU receiving progress changed concurrently");
|
||||
}
|
||||
@@ -623,21 +646,14 @@ public class ReceivingContainerService {
|
||||
private void validateFbaSkuBalance(String customerId, String containerNo) {
|
||||
for (InboundRef ref : refs(customerId, containerNo)) {
|
||||
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) continue;
|
||||
List<LastlegOrderDetail> details = orderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, ref.order().getId()));
|
||||
Set<String> skuCodes = details.stream().map(LastlegOrderDetail::getSkuCode)
|
||||
.map(ReceivingContainerService::trim).filter(Objects::nonNull)
|
||||
.map(value -> value.toUpperCase(java.util.Locale.ROOT))
|
||||
.collect(Collectors.toCollection(java.util.LinkedHashSet::new));
|
||||
int expected = 0;
|
||||
List<SkuExpectation> skuCodes = skuExpectations(ref);
|
||||
int received = 0;
|
||||
for (String skuCode : skuCodes) {
|
||||
SkuRef sku = requireSku(ref, skuCode);
|
||||
for (SkuExpectation sku : skuCodes) {
|
||||
int skuReceived = receivedSkuPackages(customerId, ref.line().getId(), sku.skuCode());
|
||||
if (skuReceived != sku.expectedPackages()) throw conflict("Every FBA SKU must be fully received before completing the container");
|
||||
expected += sku.expectedPackages(); received += skuReceived;
|
||||
received += skuReceived;
|
||||
}
|
||||
if (skuCodes.isEmpty() || expected != value(ref.line().getRequiredPackageCount()) || received != expected) {
|
||||
if (received != value(ref.line().getRequiredPackageCount())) {
|
||||
throw conflict("FBA SKU package snapshots must exactly match the inbound code total");
|
||||
}
|
||||
}
|
||||
@@ -790,7 +806,9 @@ public class ReceivingContainerService {
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
|
||||
private record InboundRef(InboundPlan plan, InboundPlanLine line, LastlegOrder order) { }
|
||||
private record SkuRef(InboundRef ref, String skuCode, String productName, int expectedPackages) { }
|
||||
private record CartonRef(InboundPlanCarton carton, InboundRef ref) { }
|
||||
private record SkuExpectation(String skuCode, String productName, int expectedPackages) { }
|
||||
private record FbaSkuAllocation(InboundRef ref, String skuCode, String productName, int packages) { }
|
||||
private record Totals(int inboundCount, List<String> destinationCodes, int plannedPalletCount,
|
||||
int total, int received) { }
|
||||
private record PrepareCommand(String containerNo, String idempotencyKey) { }
|
||||
|
||||
@@ -72,17 +72,10 @@ paths:
|
||||
/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve:
|
||||
post:
|
||||
summary: 解析当前柜箱唛
|
||||
description: 权限 lastleg:pda:operate;仅 OVERSEAS 目的类型可逐箱收货,返回该箱所属入仓码完整信息。
|
||||
description: 权限 lastleg:pda:operate;FBA 和 OVERSEAS 均可解析当前锁定柜内箱码,返回唯一入仓码、目的类型、目标仓和实收进度;不返回 SKU。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { type: object, required: [cartonMark], properties: { cartonMark: { type: string } } } } } }
|
||||
responses: { '200': { description: 箱唛与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCartonResolvedApiResponse' } } } }, '409': { description: 跨柜、重复箱或目的类型不符 } }
|
||||
/api/pda/v1/last-leg/receiving-containers/current/skus/resolve:
|
||||
post:
|
||||
summary: 解析当前柜 FBA SKU
|
||||
description: 权限 lastleg:pda:operate;仅解析当前已签柜内唯一归属一个入仓码的 FBA SKU,返回冻结箱数与实收进度。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { type: object, required: [skuCode], properties: { skuCode: { type: string, minLength: 1, maxLength: 128 } } } } } }
|
||||
responses: { '200': { description: SKU与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingSkuResolvedApiResponse' } } } }, '404': { description: A0404:SKU不在当前FBA柜内 }, '409': { description: A0409:SKU跨多个入仓码导致归属不唯一 } }
|
||||
responses: { '200': { description: 箱唛与入仓码信息, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingCartonResolvedApiResponse' } } } }, '409': { description: 箱码跨柜或归属不唯一 } }
|
||||
/api/pda/v1/last-leg/receiving-containers/current/pallets/resolve:
|
||||
post:
|
||||
summary: 解析当前柜实物托盘码
|
||||
@@ -93,7 +86,7 @@ paths:
|
||||
/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk:
|
||||
post:
|
||||
summary: FBA 入仓码按箱数收货
|
||||
description: 权限 lastleg:pda:operate;只允许 destinationType=FBA,OVERSEAS 在服务端硬拒绝;不接收 customerId、planNo、allocationId、warehouseId、locationId。
|
||||
description: 权限 lastleg:pda:operate;只允许 destinationType=FBA,OVERSEAS 在服务端硬拒绝;服务端按当前入仓码所属计划的 line.id ASC 和冻结 SKU 明细顺序拆分箱数并记录真实 SKU 收货;不接收 SKU、customerId、planNo、allocationId、warehouseId、locationId。
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingBulk' } } } }
|
||||
responses: { '200': { description: 本次与累计实收进度, content: { application/json: { schema: { $ref: '#/components/schemas/ReceivingReceiptApiResponse' } } } }, '409': { description: 海外仓批量、超收或托盘不匹配 } }
|
||||
@@ -658,10 +651,9 @@ components:
|
||||
downloadPath: { type: string, description: 用于会话恢复时直接下载签收照片 }
|
||||
ReceivingBulk:
|
||||
type: object
|
||||
required: [inboundCode, skuCode, palletCode, packageCount, idempotencyKey]
|
||||
required: [inboundCode, palletCode, packageCount, idempotencyKey]
|
||||
properties:
|
||||
inboundCode: { type: string, minLength: 1 }
|
||||
skuCode: { type: string, minLength: 1, maxLength: 128 }
|
||||
palletCode: { type: string, minLength: 1 }
|
||||
packageCount: { type: integer, minimum: 1 }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
@@ -751,15 +743,6 @@ components:
|
||||
required: [cartonMark]
|
||||
properties:
|
||||
cartonMark: { type: string }
|
||||
skuCode: { type: string, nullable: true }
|
||||
ReceivingSkuResolved:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ReceivingInbound' }
|
||||
- type: object
|
||||
required: [skuCode]
|
||||
properties:
|
||||
skuCode: { type: string }
|
||||
productName: { type: string, nullable: true }
|
||||
ReceivingPalletResolved:
|
||||
type: object
|
||||
required: [palletCode, warehouseId, status, inboundCode, destinationType, compatible]
|
||||
@@ -814,12 +797,6 @@ components:
|
||||
- type: object
|
||||
required: [data]
|
||||
properties: { data: { $ref: '#/components/schemas/ReceivingCartonResolved' } }
|
||||
ReceivingSkuResolvedApiResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||
- type: object
|
||||
required: [data]
|
||||
properties: { data: { $ref: '#/components/schemas/ReceivingSkuResolved' } }
|
||||
ReceivingPalletResolvedApiResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/ReceivingApiResponseBase' }
|
||||
|
||||
+7
-3
@@ -126,7 +126,7 @@ class ReceivingContainerContractTest {
|
||||
assertTrue(yaml.contains(prefix + "/candidates:"));
|
||||
assertTrue(yaml.contains(prefix + "/warehouse-profile:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/receipts/bulk:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/skus/resolve:"));
|
||||
assertFalse(yaml.contains(prefix + "/current/skus/resolve:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/receipts/carton:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/complete:"));
|
||||
assertTrue(yaml.contains(prefix + "/current/unload-photos/files/{fileId}/download:"));
|
||||
@@ -134,8 +134,8 @@ class ReceivingContainerContractTest {
|
||||
String receivingPaths = yaml.substring(yaml.indexOf(prefix), yaml.indexOf("/api/platform/v1/last-leg/orders:"));
|
||||
List<String> successResponses = receivingPaths.lines().map(String::trim)
|
||||
.filter(line -> line.startsWith("responses: { '200':")).toList();
|
||||
assertEquals(17, successResponses.size());
|
||||
assertEquals(13, successResponses.stream().filter(line -> line.contains("/schemas/Receiving")).count());
|
||||
assertEquals(16, successResponses.size());
|
||||
assertEquals(12, successResponses.stream().filter(line -> line.contains("/schemas/Receiving")).count());
|
||||
assertEquals(4, successResponses.stream().filter(line -> line.contains("/schemas/Pda")).count());
|
||||
assertTrue(receivingPaths.contains("返回 WAIT_SIGN、SIGNED 或 RECEIVING 当前柜"));
|
||||
assertTrue(receivingPaths.contains("仅 PLATFORM WAREHOUSE_PICKER 可用"));
|
||||
@@ -146,6 +146,10 @@ class ReceivingContainerContractTest {
|
||||
assertTrue(yaml.contains("overseasWarehouseId: { type: string"));
|
||||
assertTrue(yaml.contains("overseasWarehouseName: { type: string"));
|
||||
assertFalse(receivingPaths.contains("photoFileIds"));
|
||||
assertFalse(receivingPaths.contains("skuCode"));
|
||||
assertFalse(yaml.contains("ReceivingSkuResolved"));
|
||||
assertFalse(Arrays.stream(ReceivingContainerRequests.class.getDeclaredClasses())
|
||||
.anyMatch(type -> "SkuResolve".equals(type.getSimpleName())));
|
||||
}
|
||||
|
||||
private String resource(String name) throws IOException {
|
||||
|
||||
+57
-6
@@ -182,7 +182,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
void destinationModesAreHardRejectedBeforeWarehouseCalls() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "SKU-1", "PAL-1", 1, "bulk-key")))
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "bulk-key")))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
verifyNoInteractions(warehouse);
|
||||
|
||||
@@ -209,7 +209,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
stubSku();
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(101L, "FBA-1"));
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "SKU-1", "PAL-1", 1, "pallet-key")))
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "pallet-key")))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||
}
|
||||
@@ -223,7 +223,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "over receipt"))
|
||||
.when(workflow).validateBulkResolved(any(), any(), any(), anyInt());
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "SKU-1", "PAL-1", 2, "over-key")))
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 2, "over-key")))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||
|
||||
@@ -291,7 +291,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
doThrow(failure).when(workflow).receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any());
|
||||
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "SKU-1", "PAL-1", 1, "compensate-key")))
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "compensate-key")))
|
||||
.isSameAs(failure);
|
||||
verify(workflow).compensateReservation("100", 55L, failure);
|
||||
}
|
||||
@@ -391,7 +391,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkRejectsSkuOverReceiptBeforeWarehouseLookup() {
|
||||
void bulkRejectsAggregateSkuOverReceiptBeforeWarehouseLookup() {
|
||||
stubCurrent("FBA", "CONT-A");
|
||||
stubSku();
|
||||
com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt received =
|
||||
@@ -400,11 +400,62 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
when(skuReceipts.listLineSku("100", 10L, "SKU-1")).thenReturn(List.of(received));
|
||||
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "SKU-1", "PAL-1", 1, "sku-over")))
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 1, "sku-over")))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
|
||||
verifyNoInteractions(warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonResolutionSupportsFbaWithoutProjectingSku() {
|
||||
stubCurrent("FBA", "CONT-A");
|
||||
when(cartons.selectOne(any())).thenReturn(carton(1L, "FBA-BOX-1"));
|
||||
|
||||
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("FBA-BOX-1"));
|
||||
|
||||
assertThat(resolved.cartonMark()).isEqualTo("FBA-BOX-1");
|
||||
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
|
||||
assertThat(resolved.destinationType()).isEqualTo("FBA");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fbaBulkDistributesAcrossLinesAndFrozenSkusInStableLineOrder() {
|
||||
InboundPlan plan = plan("CONT-A");
|
||||
InboundPlanLine first = line("FBA");
|
||||
InboundPlanLine second = line("FBA");
|
||||
second.setId(20L); second.setOrderId(2L); second.setRequiredPackageCount(2);
|
||||
LastlegOrder firstOrder = order("FBA");
|
||||
LastlegOrder secondOrder = order("FBA"); secondOrder.setId(2L);
|
||||
when(sessions.lockActiveByOperator("OPERATOR:7")).thenReturn(session("CONT-A", "SIGNED"));
|
||||
when(plans.selectList(any())).thenReturn(List.of(plan));
|
||||
when(lines.selectList(any())).thenReturn(List.of(second, first));
|
||||
when(orders.selectById(1L)).thenReturn(firstOrder);
|
||||
when(orders.selectById(2L)).thenReturn(secondOrder);
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(detail("SKU-A", 1), detail("SKU-B", 1)),
|
||||
List.of(detail("SKU-C", 2)));
|
||||
when(warehouse.resolvePallet("PAL-1")).thenReturn(resource(100L, "FBA-1"));
|
||||
when(pallets.selectList(any())).thenReturn(List.of(planPallet()));
|
||||
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(4));
|
||||
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
|
||||
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
|
||||
|
||||
var result = service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 3, "split-key"));
|
||||
|
||||
assertThat(result.acceptedPackages()).isEqualTo(3);
|
||||
ArgumentCaptor<InboundPlanLine> capturedLines = ArgumentCaptor.forClass(InboundPlanLine.class);
|
||||
ArgumentCaptor<Integer> capturedCounts = ArgumentCaptor.forClass(Integer.class);
|
||||
verify(workflow, org.mockito.Mockito.times(3)).receiveBulkResolved(any(), any(), capturedLines.capture(), any(),
|
||||
capturedCounts.capture(), any(), any());
|
||||
assertThat(capturedLines.getAllValues()).containsExactly(first, first, second);
|
||||
assertThat(capturedCounts.getAllValues()).containsExactly(1, 1, 1);
|
||||
ArgumentCaptor<com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt> receipts =
|
||||
ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt.class);
|
||||
verify(skuReceipts, org.mockito.Mockito.times(3)).insert(receipts.capture());
|
||||
assertThat(receipts.getAllValues()).extracting(value -> value.getSkuCode())
|
||||
.containsExactly("SKU-A", "SKU-B", "SKU-C");
|
||||
}
|
||||
|
||||
private void stubCurrent(String destinationType, String containerNo) {
|
||||
InboundPlan plan = plan(containerNo);
|
||||
InboundPlanLine line = line(destinationType);
|
||||
|
||||
@@ -768,10 +768,18 @@ export class AcceptanceRun {
|
||||
for (let sequence = 2; sequence <= total; sequence += 1) {
|
||||
this.marker = `${baseMarker}-WS${String(sequence).padStart(2, '0')}`
|
||||
this.applyWaitSplitVariant(sequence)
|
||||
const orderStepStart = this.steps.length
|
||||
await this.executeModule('orders', () => orders.run(this))
|
||||
if (this.modules.at(-1)?.success !== true) break
|
||||
const orderRun = this.modules.at(-1)
|
||||
const orderBlockingFailure = Boolean(orderRun?.fatal)
|
||||
|| this.steps.slice(orderStepStart).some((step) => !step.passed && step.deploymentBlocker)
|
||||
if (orderBlockingFailure) break
|
||||
const measurementStepStart = this.steps.length
|
||||
await this.executeModule('inbound-measurement', () => measurement.run(this))
|
||||
if (this.modules.at(-1)?.success !== true || !this.captureWaitSplitFixture(sequence)) break
|
||||
const measurementRun = this.modules.at(-1)
|
||||
const measurementBlockingFailure = Boolean(measurementRun?.fatal)
|
||||
|| this.steps.slice(measurementStepStart).some((step) => !step.passed && step.deploymentBlocker)
|
||||
if (measurementBlockingFailure || !this.captureWaitSplitFixture(sequence)) break
|
||||
}
|
||||
} finally {
|
||||
this.marker = baseMarker
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function run(context) {
|
||||
beforeContainerVersion: Number(container?.version), beforeItems,
|
||||
expectedAssignments: source.expectedAssignments || [],
|
||||
instructions: [
|
||||
'在订舱页面创建与本清单客户、起运仓、渠道和柜型一致的 DRAFT 订舱,并填写完整运输资料。',
|
||||
'在订舱页面创建与本清单客户、起运仓、渠道和柜型一致的 DRAFT 订舱,并填写承运船司代码 carrierCode 及完整运输资料。',
|
||||
'确认放舱,使订舱进入 RELEASED;放舱本身不得创建货柜。',
|
||||
'在原 containerBookingId 上选择该订舱未占用的柜型需求,录入唯一真实 containerNo 完成关联。',
|
||||
'回到预装详情,确认 canConfirm=true 后执行整份计划确认;不要领取 PDA 任务。',
|
||||
|
||||
@@ -137,7 +137,7 @@ async function createConfirmedContainer(context, source, label) {
|
||||
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
|
||||
channelCode: summary.channelCode, warehouseId: source.warehouseId,
|
||||
billOfLadingNo: `BL-${value}`.slice(0, 128),
|
||||
shippingCompanyId: id(agent.id), shippingCompanyName: agent.name,
|
||||
shippingCompanyId: id(agent.id), shippingCompanyName: agent.name, carrierCode: 'OTHER',
|
||||
bookingAgentId: id(agent.id), bookingAgentName: agent.name, departurePortCode: 'CNSZX',
|
||||
vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64),
|
||||
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
||||
|
||||
@@ -39,6 +39,9 @@ test('批量待分柜参数固定同一客户并限制为订单测量链', () =>
|
||||
assert.match(acceptance, /wait-split-count必须是1-100的整数/)
|
||||
assert.match(acceptance, /批量WAIT_SPLIT只允许执行merchant-qualification-fixture、orders、inbound-measurement/)
|
||||
assert.match(acceptance, /createAdditionalWaitSplitFixtures/)
|
||||
assert.match(acceptance, /orderBlockingFailure[\s\S]*step\.deploymentBlocker/)
|
||||
assert.match(acceptance, /measurementBlockingFailure[\s\S]*step\.deploymentBlocker/)
|
||||
assert.doesNotMatch(acceptance, /this\.modules\.at\(-1\)\?\.success !== true/)
|
||||
assert.match(acceptance, /measurement\.processStatus === 'WAIT_SPLIT'/)
|
||||
assert.match(acceptance, /measurement\.finalStatus === 'APPROVED'/)
|
||||
assert.match(acceptance, /waitSplitVariant\(sequence\)/)
|
||||
|
||||
@@ -28,6 +28,7 @@ test('shared DRAFT uses requirements and never registers a real container number
|
||||
assert.equal(JSON.stringify(draft).includes('containerNo'), false)
|
||||
assert.equal(draft.serviceTerm, 'CY-CY')
|
||||
assert.equal(draft.freightTerm, 'NINGBO')
|
||||
assert.equal(draft.carrierCode, 'OTHER')
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) assert.ok(draft[field])
|
||||
})
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ export function oceanBookingDraft(context, suffix, master, labels = {}) {
|
||||
return {
|
||||
...writableMaster,
|
||||
billOfLadingNo: labels.billOfLadingNo || `BL-${suffix}`,
|
||||
carrierCode: labels.carrierCode || 'OTHER',
|
||||
shippingCompanyId: labels.shippingCompanyId ?? null,
|
||||
shippingCompanyName: labels.shippingCompanyId ? labels.shippingCompanyName ?? null : null,
|
||||
bookingAgentId: labels.bookingAgentId ?? null,
|
||||
|
||||
@@ -159,7 +159,7 @@ export async function run(context) {
|
||||
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
|
||||
channelCode, warehouseId,
|
||||
billOfLadingNo: `BL-${suffix}`.slice(0, 128),
|
||||
shippingCompanyId: id(bookingAgent.id), shippingCompanyName: bookingAgent.name,
|
||||
shippingCompanyId: id(bookingAgent.id), shippingCompanyName: bookingAgent.name, carrierCode: 'OTHER',
|
||||
bookingAgentId: id(bookingAgent.id), bookingAgentName: bookingAgent.name,
|
||||
vesselName: `验收船-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64),
|
||||
departurePortCode: 'CNSZX', departurePortName: '深圳',
|
||||
|
||||
@@ -175,7 +175,7 @@ export async function run(context) {
|
||||
warehouseId,
|
||||
channelCode: initial?.summary?.channelCode,
|
||||
billOfLadingNo: `P3BL-${suffix}`.slice(0, 128),
|
||||
shippingCompanyId: id(shippingCarrier.id), shippingCompanyName: shippingCarrier.name,
|
||||
shippingCompanyId: id(shippingCarrier.id), shippingCompanyName: shippingCarrier.name, carrierCode: 'OTHER',
|
||||
bookingAgentId: id(supplier.id), bookingAgentName: supplier.name,
|
||||
vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64),
|
||||
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
|
||||
|
||||
@@ -263,7 +263,7 @@ node scripts/admin-test-env-acceptance.mjs `
|
||||
|
||||
严格按以下顺序操作:
|
||||
|
||||
1. 创建与准备清单客户、起运仓、渠道、柜型一致的海运订舱 DRAFT,并补齐提单号、船名、航次、ETD、ETA、参与方和柜型需求。
|
||||
1. 创建与准备清单客户、起运仓、渠道、柜型一致的海运订舱 DRAFT,并补齐承运船司代码 `carrierCode`、提单号、船名、航次、ETD、ETA、参与方和柜型需求。
|
||||
2. 点击放舱,使订舱进入 `RELEASED`。此时不应自动生成真实柜次或 PDA 任务。
|
||||
3. 在原内部柜次上选择该订舱未占用的柜型需求,录入唯一真实 `containerNo` 完成关联;不得新建替代柜次。
|
||||
4. 回到预装详情,确认门禁变为 `canConfirm=true`,再执行整份预装计划确认。
|
||||
|
||||
Reference in New Issue
Block a user