贴标申请,仓储转运
This commit is contained in:
+5
-1
@@ -18,6 +18,7 @@ import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
import com.baiyue.lastleg.service.LastlegOrderCommandService;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnReceiptService;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
@@ -50,6 +51,7 @@ import java.util.List;
|
||||
@RestController
|
||||
@Tag(name = "Tail-leg orders")
|
||||
public class LastlegOrderController {
|
||||
@Resource private ReturnReceiptService returnReceiptService;
|
||||
@Resource private LoadingCompletedConsumer consumer;
|
||||
@Resource private ContainerShippingNodeConsumer shippingNodeConsumer;
|
||||
@Resource private LastlegOrderService orderService;
|
||||
@@ -165,7 +167,9 @@ public class LastlegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id) {
|
||||
return ApiResponse.success(orderService.detail(platformScope(user, customerId), id));
|
||||
String scopedCustomerId = platformScope(user, customerId);
|
||||
returnReceiptService.ensureTailCargo(scopedCustomerId, id);
|
||||
return ApiResponse.success(orderService.detail(scopedCustomerId, id));
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/relabel-review")
|
||||
|
||||
+19
-3
@@ -78,7 +78,18 @@ public final class DeliveryRequests {
|
||||
@NotBlank @Size(max=128) String returnNo,
|
||||
@NotNull @Size(min=1, max=200) List<@NotBlank @Size(max=128) String> cartonMarks,
|
||||
@Size(max=1000) String remark,
|
||||
@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
@NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=128) String skuCode,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType) {
|
||||
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
|
||||
List<String> cartonMarks, String remark, String idempotencyKey) {
|
||||
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, null, null);
|
||||
}
|
||||
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
|
||||
List<String> cartonMarks, String remark, String idempotencyKey, String skuCode) {
|
||||
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, null);
|
||||
}
|
||||
}
|
||||
public record WarehouseTransferOutbound(
|
||||
@NotBlank @Size(max=64) String transferNo,
|
||||
@NotNull @Size(min=1, max=100) List<@NotNull Long> palletIds,
|
||||
@@ -102,9 +113,14 @@ public final class DeliveryRequests {
|
||||
}
|
||||
public record PdaPick(@NotNull Long outboundPalletItemId, Long inventoryPositionId,
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks) {
|
||||
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks,
|
||||
@Size(max=128) String skuCode) {
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of());
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of(), null);
|
||||
}
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages,
|
||||
String idempotencyKey, List<String> cartonMarks) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, cartonMarks, null);
|
||||
}
|
||||
}
|
||||
public record PdaPalletPick(@NotBlank @Size(max=255) String idempotencyKey) {}
|
||||
|
||||
+11
-4
@@ -41,7 +41,15 @@ public final class DeliveryResponses {
|
||||
public record DeliverySku(String inboundCode, String customerName, String destinationWarehouseCode,
|
||||
String receivingAddress, String shipmentNo, String fbaTrackingNo,
|
||||
String skuCode, String productName, int packageCount,
|
||||
BigDecimal weightKg, BigDecimal volumeCbm) {}
|
||||
BigDecimal weightKg, BigDecimal volumeCbm, Long productQuantity) {
|
||||
public DeliverySku(String inboundCode, String customerName, String destinationWarehouseCode,
|
||||
String receivingAddress, String shipmentNo, String fbaTrackingNo,
|
||||
String skuCode, String productName, int packageCount,
|
||||
BigDecimal weightKg, BigDecimal volumeCbm) {
|
||||
this(inboundCode, customerName, destinationWarehouseCode, receivingAddress, shipmentNo,
|
||||
fbaTrackingNo, skuCode, productName, packageCount, weightKg, volumeCbm, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record DeliveryFeeRow(Long id, String feeDirection, String feeType, String currency,
|
||||
BigDecimal amount, String remark, String reviewStatus, String sourceStatus, String voidStatus) {}
|
||||
@@ -65,7 +73,7 @@ public final class DeliveryResponses {
|
||||
String skuCode, String productName, long availablePackages,
|
||||
BigDecimal grossWeightKg, BigDecimal volumeWeightKg, BigDecimal volumeCbm) {}
|
||||
public record ReturnOutboundCandidate(String returnNo, String overseasWarehouseId,
|
||||
String cartonMark, String fbaCartonMark,
|
||||
String cartonMark, String palletCode, String fbaCartonMark,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
long totalQuantity, String status) {}
|
||||
public record WarehouseTransferOutboundCandidate(String customerId, String customerName,
|
||||
@@ -78,8 +86,7 @@ public final class DeliveryResponses {
|
||||
Long palletId, Long warehousePalletId, String palletNo,
|
||||
int packageCount, BigDecimal grossWeightKg,
|
||||
BigDecimal volumeCbm) {}
|
||||
public record PdaTask(Long id, String taskNo, String status, String outboundNo, String outboundType,
|
||||
LocalDateTime pushedAt, LocalDateTime completedAt) {}
|
||||
public record PdaTask(Long id, String taskNo, String status, String outboundNo, LocalDateTime pushedAt, LocalDateTime completedAt, String outboundType) {}
|
||||
public record PdaTaskResolution(String workflow, String taskNo, String outboundNo, String dispatchNo) {}
|
||||
public record PdaPickPosition(Long inventoryPositionId, String palletNo, String cartonMark,
|
||||
String skuCode, String inboundCode, long availablePackages,
|
||||
|
||||
+202
-264
@@ -1,9 +1,5 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard;
|
||||
import com.baiyue.lastleg.fleet.trip.FleetDispatchGateway;
|
||||
import com.baiyue.lastleg.fleet.trip.FleetBookingFreight;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -114,6 +110,7 @@ public class DeliveryTransferService {
|
||||
@Resource private DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService returnStock;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private InboundPlanPalletAllocationMapper planAllocations;
|
||||
@Resource private InboundPlanPalletMapper inboundPlanPallets;
|
||||
@Resource private WarehouseResourceGateway warehouseResources;
|
||||
@@ -122,31 +119,11 @@ public class DeliveryTransferService {
|
||||
@Resource private LastlegOrderMapper tailOrders;
|
||||
@Resource private LastlegOrderStatusHistoryMapper histories;
|
||||
@Resource private LastlegOrderDetailMapper tailOrderDetails;
|
||||
@Resource private com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaPickOrderMapper dispatchPickOrders;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelApplicationService relabelService;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private FleetDispatchBindingGuard fleetBindings;
|
||||
@Resource private FleetBookingFreight bookingFreight;
|
||||
@Resource private DeliveryFeeCoverageService feeCoverage;
|
||||
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.REPEATABLE_READ)
|
||||
public FleetBookingFreight.Preview externalFreight(String dispatchNo) {
|
||||
DispatchOrder dispatch = dispatchOrders.findGlobalByDispatchNo(trim(dispatchNo));
|
||||
if (dispatch == null) throw notFound("Dispatch order not found");
|
||||
return externalFreight(dispatch, dispatchItems.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
}
|
||||
|
||||
private FleetBookingFreight.Preview externalFreight(DispatchOrder dispatch, List<DispatchItem> items) {
|
||||
return bookingFreight.preview(dispatch.getId(), dispatch.getDispatchNo(), dispatch.getCustomerId(), items.stream()
|
||||
.map(item -> new FleetDispatchGateway.SourceCargo(item.getInboundCode(),
|
||||
item.getWarehousePalletId() == null ? null : item.getWarehousePalletId().toString(), item.getPalletNo(),
|
||||
Objects.requireNonNullElse(item.getPackageCount(), 0), FleetDispatchGateway.destinationKey(item.getDestinationType(),
|
||||
item.getDestinationWarehouseCode(), item.getFbaWarehouseCode(), item.getDeliveryAddress(), item.getPostalCode())))
|
||||
.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.TransferResult platformTransfer(String dispatchNo, String operatorId,
|
||||
@@ -159,8 +136,6 @@ public class DeliveryTransferService {
|
||||
@Transactional
|
||||
public DeliveryResponses.TransferResult merchantTransfer(String customerId, String dispatchNo, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
if (request.feeCoverage() != null && !request.feeCoverage().isEmpty())
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "原运费服务覆盖由平台确认");
|
||||
DispatchOrder found = dispatchOrders.findByDispatchNo(customerId, dispatchNo);
|
||||
if (found == null) throw notFound("Dispatch order not found");
|
||||
DispatchOrder dispatch = dispatchOrders.lockById(customerId, found.getId());
|
||||
@@ -171,20 +146,16 @@ public class DeliveryTransferService {
|
||||
private DeliveryResponses.TransferResult transferResolved(DispatchOrder dispatch, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
String customerId = dispatch.getCustomerId();
|
||||
String requestHash = transferRequestHash(request);
|
||||
OutboundOrder replay = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
if (!dispatch.getId().equals(replay.getDispatchOrderId())) throw conflict("Idempotency key belongs to another dispatch");
|
||||
return replayTransfer(replay, requestHash);
|
||||
return result(replay, requireDeliveries(customerId, replay.getId()), true);
|
||||
}
|
||||
OutboundOrder existing = outboundOrders.lockByDispatch(customerId, dispatch.getId());
|
||||
if (existing != null) return replayTransfer(existing, requestHash);
|
||||
if (existing != null) return result(existing, requireDeliveries(customerId, existing.getId()), true);
|
||||
if (Set.of("CANCELLED", "DEPARTED", "PARTIAL_DEPARTED", "COMPLETED").contains(dispatch.getStatus())) {
|
||||
throw conflict("Dispatch cannot be transferred to delivery in its current status");
|
||||
}
|
||||
fleetBindings.requireMatchingAssignment(dispatch.getId(),
|
||||
new FleetDispatchGateway.Assignment(dispatch.getVehiclePlate(), dispatch.getDriverName(), dispatch.getDriverPhone()),
|
||||
new FleetDispatchGateway.Assignment(request.vehiclePlate(), request.driverName(), request.driverPhone()));
|
||||
dispatchItems.bindReceivedPallets(customerId, dispatch.getId());
|
||||
List<DispatchItem> items = dispatchItems.findByDispatchOrder(customerId, dispatch.getId());
|
||||
if (items.isEmpty()) throw conflict("Dispatch has no pallets");
|
||||
@@ -192,16 +163,7 @@ public class DeliveryTransferService {
|
||||
throw conflict("All dispatch pallets must be received before transfer to delivery");
|
||||
}
|
||||
if (!StringUtils.hasText(dispatch.getOverseasWarehouseId())) throw conflict("Dispatch warehouse is required");
|
||||
var freight = externalFreight(dispatch, items);
|
||||
if ((!freight.quotes().isEmpty() || StringUtils.hasText(request.externalFreightHash()))
|
||||
&& !Objects.equals(freight.snapshotHash(), request.externalFreightHash()))
|
||||
throw conflict("外接实收或约定运费已变化,请重新打开转派送核对");
|
||||
var quotedIncomingNos = freight.quotes().stream().map(FleetBookingFreight.Quote::incomingNo).collect(Collectors.toSet());
|
||||
var externalItems = items.stream().filter(item -> quotedIncomingNos.contains(item.getInboundCode())).map(DispatchItem::getId).collect(Collectors.toSet());
|
||||
var coverage = request.feeCoverage() == null || request.feeCoverage().isEmpty() ? DeliveryFeeCoverageService.Decision.none()
|
||||
: feeCoverage.confirm(dispatch.getDispatchNo(), request, operatorId, externalItems);
|
||||
validateTransferFees(request, freight.requiresDeliveryFee(), coverage.includedServices());
|
||||
if (!freight.priorInboundCodes().isEmpty()) feeSources.requireNoActiveDeliveryReceivables(customerId, freight.priorInboundCodes());
|
||||
validateTransferFees(request);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
@@ -212,15 +174,10 @@ public class DeliveryTransferService {
|
||||
outbound.setLxCodes(resolveLxCodes(customerId, items));
|
||||
outbound.setPackageCount(items.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
|
||||
outbound.setPalletCount(items.size());
|
||||
outbound.setGrossWeightKg(sumKnownPhysicalWeight(items, DispatchItem::getGrossWeightKg));
|
||||
outbound.setGrossWeightKg(sum(items, DispatchItem::getGrossWeightKg));
|
||||
outbound.setVolumeWeightKg(sum(items, DispatchItem::getVolumeWeightKg));
|
||||
outbound.setVolumeCbm(sum(items, DispatchItem::getVolumeCbm));
|
||||
outbound.setIdempotencyKey(request.idempotencyKey().trim());
|
||||
outbound.setRequestHash(requestHash);
|
||||
if (coverage.evidence() != null) {
|
||||
try { outbound.setFeeCoverageJson(objectMapper.writeValueAsString(coverage.evidence())); }
|
||||
catch (JsonProcessingException exception) { throw new IllegalStateException("费用覆盖确认无法保存", exception); }
|
||||
}
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now);
|
||||
try {
|
||||
outboundOrders.insert(outbound);
|
||||
@@ -229,11 +186,11 @@ public class DeliveryTransferService {
|
||||
// the document created by the winning transaction rather than receive a false failure.
|
||||
OutboundOrder concurrent = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (concurrent != null && dispatch.getId().equals(concurrent.getDispatchOrderId())) {
|
||||
return replayTransfer(concurrent, requestHash);
|
||||
return result(concurrent, requireDeliveries(customerId, concurrent.getId()), true);
|
||||
}
|
||||
concurrent = outboundOrders.lockByDispatch(customerId, dispatch.getId());
|
||||
if (concurrent != null) {
|
||||
return replayTransfer(concurrent, requestHash);
|
||||
return result(concurrent, requireDeliveries(customerId, concurrent.getId()), true);
|
||||
}
|
||||
throw conflict("Dispatch was concurrently transferred to delivery");
|
||||
}
|
||||
@@ -294,57 +251,25 @@ public class DeliveryTransferService {
|
||||
delivery.setPackageCount(groupItems.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
|
||||
delivery.setPalletCount(groupItems.size());
|
||||
delivery.setTotalVolumeCbm(sum(groupItems, DispatchItem::getVolumeCbm));
|
||||
delivery.setTotalWeightKg(sumKnownPhysicalWeight(groupItems, DispatchItem::getGrossWeightKg)); delivery.setExpectedArrivalAt(dispatch.getEstimatedArrivalAt()); delivery.setRemark(trim(request.remark()));
|
||||
delivery.setTotalWeightKg(sum(groupItems, DispatchItem::getGrossWeightKg)); delivery.setExpectedArrivalAt(dispatch.getEstimatedArrivalAt()); delivery.setRemark(trim(request.remark()));
|
||||
delivery.setIdempotencyKey(childKey(request.idempotencyKey(), "DEST-" + groupIndex++)); delivery.setCreatedBy(operatorId);
|
||||
delivery.setCreatedAt(now); delivery.setUpdatedAt(now); deliveryOrders.insert(delivery);
|
||||
recordOperation(customerId, delivery.getId(), operatorId, "CREATE_DELIVERY", "按目的FBA仓生成派送单", now);
|
||||
bindTailOrders(customerId, groupItems, outbound.getOutboundNo(), delivery.getDeliveryNo(), request.remark(), now);
|
||||
deliveries.add(delivery);
|
||||
}
|
||||
for (var quote : freight.quotes()) {
|
||||
var source = items.stream().filter(item -> quote.incomingNo().equals(item.getInboundCode())).findFirst().orElseThrow();
|
||||
var destination = deliveries.stream().filter(item -> deliveryGroupKey(source).equals(item.getDeliveryGroupKey())).findFirst().orElseThrow();
|
||||
recordOperation(customerId, destination.getId(), operatorId, "CONFIRM_EXTERNAL_FREIGHT",
|
||||
"外接预订 " + quote.bookingNo() + ";实收确认版本 " + quote.recordVersion() + ";约定运费 "
|
||||
+ quote.currency() + " " + quote.amount().toPlainString(), now);
|
||||
// An explicitly agreed free shipment remains a commercial fact, not a fabricated zero fee.
|
||||
if (quote.amount().signum() == 0) continue;
|
||||
feeSources.publishApproved(operatorId, "FLEET:" + quote.bookingNo() + ":" + dispatch.getDispatchNo() + ":FREIGHT",
|
||||
new OverseasFeeSourceDtos.SaveRequest(customerId, quote.customerName(),
|
||||
"OVERSEAS_DELIVERY_FEE", "DELIVERY_ORDER", destination.getId(), destination.getDeliveryNo(),
|
||||
null, null, quote.inboundCode(), null, null, null, parseLong(destination.getOverseasWarehouseId()),
|
||||
"DELIVERY_FEE", "外接运费(" + quote.bookingNo() + ")", "RECEIVABLE", null, null, null,
|
||||
null, null, null, null, null, LocalDate.now(), quote.currency(), quote.amount(), quote.quantity(),
|
||||
quote.quantityUnit(), quote.unitPrice(), null, null));
|
||||
}
|
||||
DeliveryOrder feeDelivery = deliveries.get(0);
|
||||
LastlegOrder firstOrder = firstTailOrder(customerId, items);
|
||||
List<DeliveryRequests.FeeItem> feeInputs = request.fees() == null ? List.of() : request.fees();
|
||||
var quotedIncoming = freight.quotes().stream().map(FleetBookingFreight.Quote::incomingNo).collect(Collectors.toSet());
|
||||
for (DeliveryRequests.FeeItem input : feeInputs) {
|
||||
String feeCode = input.feeType().trim().toUpperCase(Locale.ROOT);
|
||||
var uncovered = coverage.uncovered(feeCode);
|
||||
var uncoveredItemIds = uncovered.stream().map(com.baiyue.lastleg.delivery.dto.DeliveryFeeCoverageModels.Cargo::dispatchItemId).collect(Collectors.toSet());
|
||||
var chargeItems = coverage.evidence() != null && "RECEIVABLE".equals(input.direction())
|
||||
? items.stream().filter(item -> uncoveredItemIds.contains(item.getId())).toList()
|
||||
: "RECEIVABLE".equals(input.direction()) && "DELIVERY_FEE".equals(feeCode) && freight.requiresDeliveryFee()
|
||||
? items.stream().filter(item -> !quotedIncoming.contains(item.getInboundCode())).toList() : items;
|
||||
var feeDestination = "DELIVERY_FEE".equalsIgnoreCase(input.feeType()) && !chargeItems.isEmpty()
|
||||
? deliveries.stream().filter(item -> deliveryGroupKey(chargeItems.get(0)).equals(item.getDeliveryGroupKey())).findFirst().orElseThrow()
|
||||
: feeDelivery;
|
||||
LastlegOrder firstOrder = firstTailOrder(customerId, chargeItems);
|
||||
if (coverage.evidence() != null && "RECEIVABLE".equals(input.direction())) {
|
||||
var uncoveredOrderIds = uncovered.stream().map(com.baiyue.lastleg.delivery.dto.DeliveryFeeCoverageModels.Cargo::orderId).filter(Objects::nonNull).distinct().toList();
|
||||
firstOrder = uncoveredOrderIds.size() == 1 ? tailOrders.selectById(uncoveredOrderIds.get(0)) : null;
|
||||
if (firstOrder != null && !customerId.equals(firstOrder.getCustomerId())) throw conflict("新增费用来源客户不一致");
|
||||
}
|
||||
feeSources.publishApproved(operatorId, childKey(request.idempotencyKey(), input.direction() + ":" + feeCode),
|
||||
new OverseasFeeSourceDtos.SaveRequest(customerId, feeDestination.getCustomerName(),
|
||||
"OVERSEAS_DELIVERY_FEE", "DELIVERY_ORDER", feeDestination.getId(), feeDestination.getDeliveryNo(),
|
||||
feeSources.publishApproved(operatorId, childKey(request.idempotencyKey(), input.feeType()),
|
||||
new OverseasFeeSourceDtos.SaveRequest(customerId, feeDelivery.getCustomerName(),
|
||||
"OVERSEAS_DELIVERY_FEE", "DELIVERY_ORDER", feeDelivery.getId(), feeDelivery.getDeliveryNo(),
|
||||
firstOrder == null ? null : firstOrder.getId(), null,
|
||||
firstOrder == null ? null : firstOrder.getInboundCode(), null,
|
||||
firstOrder == null ? null : parseLong(firstOrder.getSourceContainerBookingId()),
|
||||
firstOrder == null ? null : firstOrder.getContainerNo(), parseLong(feeDestination.getOverseasWarehouseId()),
|
||||
feeCode, input.feeName().trim(), input.direction(), input.supplierId(),
|
||||
firstOrder == null ? null : firstOrder.getContainerNo(), parseLong(feeDelivery.getOverseasWarehouseId()),
|
||||
input.feeType().trim(), input.feeName().trim(), input.direction(), input.supplierId(),
|
||||
input.supplierName(), input.supplierCategory(), input.settlementMode(), input.paidAt(),
|
||||
input.paymentMethod(), input.paymentReferenceNo(), input.paymentVoucherFileIds(), LocalDate.now(),
|
||||
input.currency(), input.amount(), input.chargeableQuantity(), input.quantityUnit(), input.unitPrice(),
|
||||
@@ -353,45 +278,27 @@ public class DeliveryTransferService {
|
||||
return result(outbound, deliveries, false);
|
||||
}
|
||||
|
||||
private DeliveryResponses.TransferResult replayTransfer(OutboundOrder outbound, String requestHash) {
|
||||
if (!requestHash.equals(outbound.getRequestHash()))
|
||||
throw conflict("转派送请求与原已保存内容不一致,请查看原单据;费用调整须通过正式费用更正流程");
|
||||
return result(outbound, requireDeliveries(outbound.getCustomerId(), outbound.getId()), true);
|
||||
}
|
||||
|
||||
String transferRequestHash(DeliveryRequests.Transfer request) {
|
||||
// A new command key cannot change an already transferred dispatch's business content.
|
||||
com.fasterxml.jackson.databind.node.ObjectNode content = objectMapper.valueToTree(request);
|
||||
content.remove("idempotencyKey");
|
||||
if (request.feeCoverage() == null || request.feeCoverage().isEmpty()) {
|
||||
content.remove("feeCoverage"); content.remove("feeCoverageCargoHash");
|
||||
}
|
||||
return hash(content);
|
||||
}
|
||||
|
||||
private void validateTransferFees(DeliveryRequests.Transfer request, boolean requiresDeliveryFee, Set<String> covered) {
|
||||
private void validateTransferFees(DeliveryRequests.Transfer request) {
|
||||
List<DeliveryRequests.FeeItem> inputs = request.fees() == null ? List.of() : request.fees();
|
||||
if (inputs.isEmpty()) throw conflict("Delivery fee is required");
|
||||
Set<String> allowed = Set.of("DELIVERY_FEE", "LOADING_FEE", "APPOINTMENT_FEE");
|
||||
Set<String> seen = new HashSet<>();
|
||||
for (DeliveryRequests.FeeItem input : inputs) {
|
||||
String feeType = input.feeType().trim().toUpperCase(Locale.ROOT);
|
||||
if (!allowed.contains(feeType)) throw conflict("Unsupported delivery fee type: " + feeType);
|
||||
if (!seen.add(input.direction() + ":" + feeType)) throw conflict("Duplicate delivery fee type: " + feeType);
|
||||
if ("RECEIVABLE".equals(input.direction()) && covered.contains(feeType)) throw conflict("本次货物的该服务已由原费用覆盖,请勿重复登记应收");
|
||||
if (!seen.add(feeType)) throw conflict("Duplicate delivery fee type: " + feeType);
|
||||
if (!Set.of("CNY", "USD").contains(input.currency().trim().toUpperCase(Locale.ROOT)))
|
||||
throw conflict("Delivery fees only support CNY or USD");
|
||||
if (input.amount().signum() <= 0) throw conflict("Delivery fee amount must be greater than zero");
|
||||
}
|
||||
if (requiresDeliveryFee && !covered.contains("DELIVERY_FEE") && !seen.contains("RECEIVABLE:DELIVERY_FEE")) throw conflict("尚未有约定运费的货物须填写派送费");
|
||||
if (!requiresDeliveryFee && inputs.stream().anyMatch(item -> "DELIVERY_FEE".equalsIgnoreCase(item.feeType().trim())
|
||||
&& "RECEIVABLE".equals(item.direction()))) throw conflict("本排车全部货物已带入外接约定运费,请勿重复填写派送应收");
|
||||
if (!seen.contains("DELIVERY_FEE")) throw conflict("Delivery fee is required");
|
||||
if (Set.of("FLOOR", "MIXED").contains(request.loadingType().trim().toUpperCase(Locale.ROOT))
|
||||
&& !covered.contains("LOADING_FEE") && !seen.contains("RECEIVABLE:LOADING_FEE")) {
|
||||
&& !seen.contains("LOADING_FEE")) {
|
||||
throw conflict("Loading fee is required for floor or mixed loading");
|
||||
}
|
||||
if ("BUY_APPOINTMENT".equalsIgnoreCase(trim(request.appointmentAccount()))) {
|
||||
if (!StringUtils.hasText(request.sellerName())) throw conflict("Seller name is required when buying an appointment");
|
||||
if (!covered.contains("APPOINTMENT_FEE") && !seen.contains("RECEIVABLE:APPOINTMENT_FEE")) throw conflict("Appointment fee is required when buying an appointment");
|
||||
if (!seen.contains("APPOINTMENT_FEE")) throw conflict("Appointment fee is required when buying an appointment");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +382,10 @@ public class DeliveryTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
String transferRequestHash(DeliveryRequests.Transfer request) {
|
||||
return hash(request);
|
||||
}
|
||||
|
||||
public List<DeliveryResponses.OneClickCandidate> oneClickCandidates(String customerId, String warehouseId) {
|
||||
return dropshipProfiles.listOutboundCandidates(customerId,
|
||||
StringUtils.hasText(warehouseId) ? warehouseId.trim() : null).stream()
|
||||
@@ -486,45 +397,96 @@ public class DeliveryTransferService {
|
||||
if (!StringUtils.hasText(warehouseId)) return List.of();
|
||||
return returnCartons.findAvailable(required(customerId, "Customer is required"), warehouseId.trim()).stream()
|
||||
.map(c -> new DeliveryResponses.ReturnOutboundCandidate(c.getSourceReturnNo(), c.getOverseasWarehouseId(),
|
||||
c.getCartonMark(), c.getFbaCartonMark(), c.getDestinationWarehouseCode(), c.getDestinationWarehouseName(),
|
||||
c.getCartonMark(), c.getPalletCode(), c.getFbaCartonMark(), c.getDestinationWarehouseCode(), c.getDestinationWarehouseName(),
|
||||
value(c.getTotalQuantity()), c.getStatus())).toList();
|
||||
}
|
||||
|
||||
/** Overseas gateway freight was removed; callers must provide delivery fees explicitly. */
|
||||
@Transactional(readOnly = true)
|
||||
public com.baiyue.lastleg.fleet.trip.FleetBookingFreight.Preview externalFreight(String dispatchNo) {
|
||||
return new com.baiyue.lastleg.fleet.trip.FleetBookingFreight.Preview(
|
||||
"", true, List.of(), List.of());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask createReturnOutbound(String customerId, String operatorId,
|
||||
DeliveryRequests.ReturnOutbound request) {
|
||||
String requestHash = hash(request);
|
||||
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId()));
|
||||
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
|
||||
String warehouseId = request.overseasWarehouseId().trim();
|
||||
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(StringUtils::hasText).distinct().toList();
|
||||
if (marks.size() != request.cartonMarks().size()) throw conflict("箱唛不能为空且不可重复");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<ReturnInventoryCarton> locked = new ArrayList<>();
|
||||
for (String mark : marks) {
|
||||
for (String mark : marks.stream().sorted().toList()) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(mark);
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId()) || !warehouseId.equals(carton.getOverseasWarehouseId())
|
||||
|| !"AVAILABLE".equals(carton.getStatus()) || !Objects.equals(request.returnNo().trim(), carton.getSourceReturnNo())) {
|
||||
|| !returnStock.matchesReturnSource(carton, request.returnNo())) {
|
||||
throw conflict("所选退货箱唛不可出库或不属于该退货单:" + mark);
|
||||
}
|
||||
locked.add(carton);
|
||||
}
|
||||
existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱已被其他作业占用:" + carton.getCartonMark());
|
||||
var contents = returnCartonItems.lockCarton(carton.getId());
|
||||
if (contents.isEmpty() || contents.stream().anyMatch(line -> value(line.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("退货箱缺少明细或已被合箱计划锁定:" + carton.getCartonMark());
|
||||
if (StringUtils.hasText(request.skuCode()) && (contents.size() != 1
|
||||
|| !request.skuCode().trim().equals(contents.get(0).getSkuCode())))
|
||||
throw conflict("单SKU出库只支持指定SKU的散件库存");
|
||||
if (StringUtils.hasText(request.skuCode()) && !"PARCEL".equals(carton.getSourceUnitType()))
|
||||
throw conflict("已装箱库存请按箱唛出库");
|
||||
if ("PALLET".equals(request.unitType()) && !StringUtils.hasText(carton.getPalletCode()))
|
||||
throw conflict("整托出库必须存在托盘绑定");
|
||||
}
|
||||
if ("PALLET".equals(request.unitType())) {
|
||||
String pallet = locked.get(0).getPalletCode();
|
||||
if (locked.stream().anyMatch(c -> !Objects.equals(pallet, c.getPalletCode()))) throw conflict("整托出库只能选择同一托盘");
|
||||
var all = returnCartons.listActivePallet(warehouseId, pallet);
|
||||
if (all.size() != marks.size() || all.stream().anyMatch(c -> !customerId.equals(c.getCustomerId())
|
||||
|| !marks.contains(c.getCartonMark()) || !"AVAILABLE".equals(c.getStatus())))
|
||||
throw conflict("整托出库必须选择该托盘全部可用箱,不可遗漏或包含其他客户库存");
|
||||
}
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RETURN_OUTBOUND", warehouseId, outbound.getId()));
|
||||
outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(marks.size()); outbound.setPalletCount(0);
|
||||
outbound.setOverseasWarehouseId(warehouseId); outbound.setIdempotencyKey(request.idempotencyKey().trim());
|
||||
outbound.setRequestHash(requestHash);
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId);
|
||||
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(1); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo()); item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo());
|
||||
item.setSkuCode(trim(request.skuCode()));
|
||||
item.setPalletNo("PALLET".equals(request.unitType()) ? carton.getPalletCode() : null);
|
||||
item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
}
|
||||
DeliveryOrder delivery = new DeliveryOrder(); delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", warehouseId, delivery.getId()));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(warehouseId); delivery.setDeliveryGroupKey("RETURN:" + request.returnNo().trim());
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setDeliveryAddress(request.recipient().trim());
|
||||
delivery.setPackageCount(marks.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
delivery.setRemark(trim(request.remark())); delivery.setIdempotencyKey(childKey(request.idempotencyKey(), "RETURN-DELIVERY"));
|
||||
delivery.setVersion(0L); delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
|
||||
deliveryOrders.insert(delivery);
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setOutboundOrderId(outbound.getId());
|
||||
task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId()); task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task);
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
private DeliveryResponses.PdaTask replayReturnOutbound(String customerId, OutboundOrder existing, String requestHash) {
|
||||
if (!"RETURN_OUTBOUND".equals(existing.getOutboundType()) || !requestHash.equals(existing.getRequestHash()))
|
||||
throw conflict("相同幂等键不能用于不同的退货出库请求");
|
||||
return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<DeliveryResponses.WarehouseTransferOutboundCandidate> warehouseTransferOutboundCandidates(
|
||||
String customerId, String warehouseId) {
|
||||
@@ -744,11 +706,11 @@ public class DeliveryTransferService {
|
||||
throw conflict("Idempotency key was reused with a different outbound pick request");
|
||||
}
|
||||
OutboundOrder replayOutbound = requireOutbound(task);
|
||||
if ("RETURN_OUTBOUND".equals(replayOutbound.getOutboundType())) {
|
||||
OutboundPalletItem replayItem = outboundPallets.selectById(replay.getOutboundPalletItemId());
|
||||
requireReturnScan(replayOutbound, replayItem, request);
|
||||
}
|
||||
return taskResult(replayOutbound, task);
|
||||
OutboundPalletItem replayItem = outboundPallets.selectById(request.outboundPalletItemId());
|
||||
if ("RETURN_OUTBOUND".equals(replayOutbound.getOutboundType())
|
||||
|| ("RELABEL_OUTBOUND".equals(replayOutbound.getOutboundType()) && replayItem != null && replayItem.getWarehousePalletId() == null))
|
||||
requireReturnScan(replayOutbound, outboundPallets.selectById(request.outboundPalletItemId()), request);
|
||||
return taskResult(requireOutbound(task), task);
|
||||
}
|
||||
OutboundOrder existingOutbound = requireOutbound(task);
|
||||
OutboundOrder outbound = outboundOrders.lockByNo(customerId, existingOutbound.getOutboundNo());
|
||||
@@ -761,16 +723,12 @@ public class DeliveryTransferService {
|
||||
if (item == null || !customerId.equals(item.getCustomerId())
|
||||
|| !task.getOutboundOrderId().equals(item.getOutboundOrderId())) throw notFound("Outbound pallet item not found");
|
||||
if (request.packages() > item.getPackageCount() - value(item.getPickedPackageCount())) throw conflict("Picked quantity exceeds outbound pallet quantity");
|
||||
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|
||||
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) {
|
||||
requireReturnScan(outbound, item, request);
|
||||
if (value(item.getPickedPackageCount()) > 0) throw conflict("退货箱唛已完成拣货");
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(item.getCartonMarks());
|
||||
requireReturnSource(outbound, item, carton);
|
||||
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱唛不可出库:" + item.getCartonMarks());
|
||||
long outboundPickId = IdWorker.getId();
|
||||
returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(),
|
||||
outbound.getOutboundNo(), outboundPickId);
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(outboundPickId); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
if (value(item.getPickedPackageCount()) > 0) throw conflict("退货箱不可重复出库");
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
|
||||
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
|
||||
updateOutboundPickingStatus(customerId, outbound);
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
@@ -934,7 +892,7 @@ public class DeliveryTransferService {
|
||||
if (outbound == null || !boundWarehouseId.equals(outbound.getOverseasWarehouseId())) {
|
||||
throw notFound("Outbound order not found in the bound warehouse");
|
||||
}
|
||||
if (!Set.of("ONE_CLICK_DROPSHIP", "RELABEL_OUTBOUND", "WAREHOUSE_TRANSFER_OUTBOUND").contains(outbound.getOutboundType())) {
|
||||
if (!Set.of("ONE_CLICK_DROPSHIP", "RELABEL_OUTBOUND", "WAREHOUSE_TRANSFER_OUTBOUND", "RETURN_OUTBOUND").contains(outbound.getOutboundType())) {
|
||||
throw conflict("整托扫码仅适用于一件代发或换标出库");
|
||||
}
|
||||
if (!Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus())) {
|
||||
@@ -947,6 +905,17 @@ public class DeliveryTransferService {
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
DeliveryResponses.PdaTask result = taskResult(outbound, task);
|
||||
for (OutboundPalletItem item : palletItems.stream().sorted(Comparator.comparing(OutboundPalletItem::getId)).toList()) {
|
||||
if (value(item.getPickedPackageCount()) >= value(item.getPackageCount())) continue;
|
||||
result = pdaPick(boundWarehouseId, operatorId, code,
|
||||
new DeliveryRequests.PdaPick(item.getId(), null, 1L,
|
||||
childKey(request.idempotencyKey(), "RETURN:" + item.getId()), List.of(item.getCartonMarks())));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<PalletPickPlan> plans = new ArrayList<>();
|
||||
int sequence = 0;
|
||||
for (OutboundPalletItem item : palletItems) {
|
||||
@@ -1052,6 +1021,10 @@ public class DeliveryTransferService {
|
||||
+ value(position.getPickedPackages()) + value(position.getReceivingPackages()))
|
||||
.sum();
|
||||
if (remaining != 0) continue;
|
||||
if (inventoryPositions.activePhysicalPallet(warehouseId, palletId) > 0) continue;
|
||||
List<String> carrierCodes = items.stream().filter(item -> palletId.equals(item.getWarehousePalletId()))
|
||||
.map(OutboundPalletItem::getPalletNo).filter(StringUtils::hasText).distinct().toList();
|
||||
if (carrierCodes.stream().anyMatch(code -> !returnCartons.listActivePallet(warehouseId, code).isEmpty())) continue;
|
||||
Long lockId = null;
|
||||
WarehouseTransferPallet transferPallet = warehouseTransferPallets.selectOne(new LambdaQueryWrapper<WarehouseTransferPallet>()
|
||||
.eq(WarehouseTransferPallet::getCustomerId, customerId)
|
||||
@@ -1070,21 +1043,20 @@ public class DeliveryTransferService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelOutbound(String outboundNo, String operatorId, DeliveryRequests.Cancel request) {
|
||||
OutboundOrder observed = outboundOrders.findGlobalByNo(trim(outboundNo));
|
||||
if (observed == null) throw notFound("Outbound order not found");
|
||||
// Dispatch picking/departure owns the same dispatch -> outbound lock order.
|
||||
DispatchOrder dispatch = observed.getDispatchOrderId() == null ? null
|
||||
: dispatchOrders.lockById(observed.getCustomerId(), observed.getDispatchOrderId());
|
||||
public void cancelOutbound(String outboundNo, DeliveryRequests.Cancel request) {
|
||||
cancelOutboundResolved(outboundNo, "PLATFORM", request);
|
||||
}
|
||||
|
||||
private void cancelOutboundResolved(String outboundNo, String operatorId, DeliveryRequests.Cancel request) {
|
||||
required(request == null ? null : request.reason(), "取消原因不能为空");
|
||||
// Confirmation locks the relabel application before its outbound document.
|
||||
// Use the same order here to avoid cancellation/confirmation deadlocks.
|
||||
OutboundOrder found = outboundOrders.findGlobalByNo(trim(outboundNo));
|
||||
RelabelApplication application = found != null && "RELABEL_OUTBOUND".equals(found.getOutboundType())
|
||||
? relabelApplications.lockByOutbound(found.getCustomerId(), found.getOutboundNo()) : null;
|
||||
OutboundOrder outbound = outboundOrders.lockGlobalByNo(trim(outboundNo));
|
||||
if (outbound == null) throw notFound("Outbound order not found");
|
||||
if ("CANCELLED".equals(outbound.getStatus())) return;
|
||||
if (outbound.getDispatchOrderId() != null) {
|
||||
if (dispatch == null || !Objects.equals(dispatch.getId(), outbound.getDispatchOrderId())
|
||||
|| !"PENDING_DEPARTURE".equals(dispatch.getStatus()) || value(dispatch.getPickedPackageCount()) > 0
|
||||
|| value(dispatch.getShippedPackageCount()) > 0) throw conflict("排车已开始执行,不能直接取消出库");
|
||||
fleetBindings.requireUnbound(dispatch.getId());
|
||||
}
|
||||
if (!Set.of("WAIT_OUTBOUND", "WAIT_PICK").contains(outbound.getStatus())) {
|
||||
throw conflict("Only an unexecuted outbound order can be cancelled");
|
||||
}
|
||||
@@ -1098,23 +1070,35 @@ public class DeliveryTransferService {
|
||||
if (existingDeliveries.stream().anyMatch(delivery -> !"WAIT_DELIVERY".equals(delivery.getStatus())))
|
||||
throw conflict("Outbound order cannot be cancelled while delivery is in progress");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (DeliveryOrder delivery : existingDeliveries) {
|
||||
int voidRequests = 0;
|
||||
for (var fee : feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId())) {
|
||||
if ("RECEIVABLE".equals(fee.getDirection()) && "DELIVERY_FEE".equals(fee.getFeeItemCode())
|
||||
&& "APPROVED".equals(fee.getReviewStatus()) && "ACTIVE".equals(fee.getSourceStatus())) {
|
||||
feeSources.voidSource(fee.getSourceFeeId(), operatorId, new OverseasFeeSourceDtos.VoidRequest(
|
||||
fee.getSourceVersion(), "取消出库 " + outboundNo + ";详见派送单取消原因",
|
||||
"CANCEL_OUTBOUND:" + outbound.getId() + ":" + fee.getSourceFeeId()));
|
||||
voidRequests++;
|
||||
if ("RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
if (application == null || !customerId.equals(application.getCustomerId())
|
||||
|| !outbound.getOutboundNo().equals(application.getOutboundOrderNo())
|
||||
|| !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus()))
|
||||
throw conflict("换标出库关联状态已变化,不能取消");
|
||||
for (OutboundPalletItem item : outboundItems) {
|
||||
if (item.getWarehousePalletId() != null) continue;
|
||||
for (String mark : Objects.toString(item.getCartonMarks(), "").split(",")) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(mark);
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !Objects.equals(application.getId(), carton.getRelabelApplicationId())
|
||||
|| !"OUTBOUND_PENDING".equals(carton.getStatus()))
|
||||
throw conflict("退货换标箱预留状态异常,不能取消出库");
|
||||
carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(now);
|
||||
returnCartons.updateById(carton);
|
||||
}
|
||||
}
|
||||
recordOperation(customerId, delivery.getId(), operatorId, "CANCEL_OUTBOUND",
|
||||
"出库取消;" + voidRequests + "笔未履行派送应收已申请撤销,其他费用保留待核对", now);
|
||||
String reason = request.reason().trim();
|
||||
for (int offset = 0; offset < reason.length(); offset += 480)
|
||||
recordOperation(customerId, delivery.getId(), operatorId, "CANCEL_OUTBOUND_REASON",
|
||||
"(" + (offset / 480 + 1) + ") " + reason.substring(offset, Math.min(offset + 480, reason.length())), now);
|
||||
relabelService.changeStatus(customerId, application.getRelabelNo(), operatorId,
|
||||
new RelabelRequests.ChangeStatus("CANCELLED", request.reason().trim()));
|
||||
}
|
||||
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
for (String mark : outboundItems.stream().map(OutboundPalletItem::getCartonMarks).sorted().toList()) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(mark);
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId()) || !"OUTBOUND_PENDING".equals(carton.getStatus()))
|
||||
throw conflict("退货箱预留状态异常,不能取消出库");
|
||||
carton.setStatus("AVAILABLE"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
}
|
||||
}
|
||||
for (DeliveryOrder delivery : existingDeliveries) {
|
||||
delivery.setStatus("CANCELLED"); delivery.setVersion(value(delivery.getVersion()) + 1);
|
||||
delivery.setUpdatedAt(now); deliveryOrders.updateById(delivery);
|
||||
}
|
||||
@@ -1139,31 +1123,19 @@ public class DeliveryTransferService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!existingDeliveries.isEmpty()) {
|
||||
var transferEvents = existingDeliveries.stream().map(delivery -> "TRANSFER_DELIVERY:" + delivery.getDeliveryNo()).toList();
|
||||
for (LastlegOrder selected : tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>().eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getOutboundOrderNo, outboundNo).orderByAsc(LastlegOrder::getId))) {
|
||||
var order = tailOrders.lockById(customerId, selected.getId());
|
||||
if (order == null || !outboundNo.equals(order.getOutboundOrderNo())) continue;
|
||||
var previous = histories.selectOne(new LambdaQueryWrapper<LastlegOrderStatusHistory>()
|
||||
.eq(LastlegOrderStatusHistory::getCustomerId, customerId).eq(LastlegOrderStatusHistory::getOrderId, order.getId())
|
||||
.in(LastlegOrderStatusHistory::getEventId, transferEvents)
|
||||
.orderByAsc(LastlegOrderStatusHistory::getCreatedAt, LastlegOrderStatusHistory::getId).last("LIMIT 1"));
|
||||
if (previous == null || !StringUtils.hasText(previous.getFromStatus())) throw conflict("缺少本次转派送前的订单状态记录,请先核对来源");
|
||||
String from = order.getCurrentStatus();
|
||||
if (tailOrders.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getId, order.getId()).eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getOutboundOrderNo, outboundNo)
|
||||
.set(LastlegOrder::getCurrentStatus, previous.getFromStatus())
|
||||
.set(LastlegOrder::getSubOrderStatus, "OUTBOUND:CANCELLED;DELIVERY:CANCELLED")
|
||||
.set(LastlegOrder::getOutboundOrderNo, null).set(LastlegOrder::getDeliveryOrderNo, null)
|
||||
.set(LastlegOrder::getVersion, value(order.getVersion()) + 1).set(LastlegOrder::getUpdatedAt, now)) != 1)
|
||||
throw conflict("订单关联已变化,请刷新后重试");
|
||||
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory(); history.setCustomerId(customerId); history.setOrderId(order.getId()); history.setFromStatus(from); history.setToStatus(previous.getFromStatus()); history.setEventId("CANCEL_OUTBOUND:" + outboundNo); history.setRemark(request.reason().trim()); history.setCreatedAt(now); histories.insert(history);
|
||||
if (!existingDeliveries.isEmpty() && !"RELABEL_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
for (LastlegOrder order : tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>().eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getOutboundOrderNo, outboundNo))) {
|
||||
String from = order.getCurrentStatus(); order.setCurrentStatus("RECEIVED"); order.setSubOrderStatus(null); order.setOutboundOrderNo(null); order.setDeliveryOrderNo(null); order.setUpdatedAt(now); tailOrders.updateById(order);
|
||||
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory(); history.setCustomerId(customerId); history.setOrderId(order.getId()); history.setFromStatus(from); history.setToStatus("RECEIVED"); history.setEventId("CANCEL_OUTBOUND:" + outboundNo); history.setRemark(request.reason().trim()); history.setCreatedAt(now); histories.insert(history);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelOutbound(String outboundNo, String operatorId, DeliveryRequests.Cancel request) {
|
||||
cancelOutboundResolved(outboundNo, operatorId, request);
|
||||
}
|
||||
|
||||
public List<DeliveryResponses.PdaTask> pdaTasks(String warehouseId, String status) {
|
||||
String boundWarehouseId = required(warehouseId, "PDA warehouse binding is required");
|
||||
return pdaTasks.listByWarehouse(boundWarehouseId, StringUtils.hasText(status) ? status.trim() : null).stream()
|
||||
@@ -1180,7 +1152,8 @@ public class DeliveryTransferService {
|
||||
task.getOutboundOrderId()).stream().map(item -> new DeliveryResponses.PdaTaskItem(item.getId(),
|
||||
item.getPalletNo(), item.getSkuCode(), item.getInboundCode(), value(item.getPackageCount()),
|
||||
value(item.getPickedPackageCount()), item.getStatus(),
|
||||
"RETURN_OUTBOUND".equals(outbound.getOutboundType()) ? returnPickPositions(outbound, task, item)
|
||||
("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|
||||
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) ? returnPickPositions(outbound, item)
|
||||
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
|
||||
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
|
||||
}
|
||||
@@ -1219,28 +1192,21 @@ public class DeliveryTransferService {
|
||||
if (item == null || !outbound.getId().equals(item.getOutboundOrderId())
|
||||
|| !outbound.getCustomerId().equals(item.getCustomerId())) throw notFound("退货出库明细不存在");
|
||||
if (request.inventoryPositionId() != null || !Long.valueOf(1).equals(request.packages()) || value(item.getPackageCount()) != 1)
|
||||
throw conflict("退货出库须逐箱扫描,无需库存仓位");
|
||||
if (request.cartonMarks() == null || request.cartonMarks().size() != 1 || request.cartonMarks().get(0) == null
|
||||
|| !Objects.equals(item.getCartonMarks(), request.cartonMarks().get(0).trim()))
|
||||
throw conflict("退货出库须逐箱或逐SKU扫描,无需库存仓位");
|
||||
if (StringUtils.hasText(item.getSkuCode())) {
|
||||
if (!Objects.equals(item.getSkuCode(), trim(request.skuCode()))) throw conflict("请扫描指定的退货SKU");
|
||||
} else if (request.cartonMarks() == null || request.cartonMarks().size() != 1
|
||||
|| !Objects.equals(item.getCartonMarks(), trim(request.cartonMarks().get(0))))
|
||||
throw conflict("请扫描当前退货出库明细指定的箱唛");
|
||||
}
|
||||
|
||||
private void requireReturnSource(OutboundOrder outbound, OutboundPalletItem item, ReturnInventoryCarton carton) {
|
||||
private List<DeliveryResponses.PdaPickPosition> returnPickPositions(OutboundOrder outbound, OutboundPalletItem item) {
|
||||
ReturnInventoryCarton carton = returnCartons.findMark(item.getCartonMarks());
|
||||
if (carton == null || !outbound.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !outbound.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|
||||
|| !Objects.equals(item.getInboundCode(), carton.getSourceReturnNo())
|
||||
|| !Objects.equals(item.getCartonMarks(), carton.getCartonMark()))
|
||||
throw conflict("退货箱唛与本次出库的客户、仓库或来源退货单不一致");
|
||||
}
|
||||
|
||||
private List<DeliveryResponses.PdaPickPosition> returnPickPositions(OutboundOrder outbound, OutboundPdaTask task,
|
||||
OutboundPalletItem item) {
|
||||
ReturnInventoryCarton carton = returnCartons.findMark(item.getCartonMarks());
|
||||
requireReturnSource(outbound, item, carton);
|
||||
boolean pending = Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus()) && value(item.getPickedPackageCount()) == 0;
|
||||
long available = pending && "AVAILABLE".equals(carton.getStatus()) ? 1 : 0;
|
||||
|| !returnStock.matchesReturnSource(carton, item.getInboundCode())) throw conflict("退货箱出库来源异常");
|
||||
return List.of(new DeliveryResponses.PdaPickPosition(null, null, item.getCartonMarks(), item.getSkuCode(),
|
||||
item.getInboundCode(), available, value(item.getPickedPackageCount())));
|
||||
item.getInboundCode(), "OUTBOUND_PENDING".equals(carton.getStatus()) ? 1 : 0, value(item.getPickedPackageCount())));
|
||||
}
|
||||
|
||||
private List<DeliveryResponses.PdaPickPosition> pdaPickPositions(String customerId, String warehouseId,
|
||||
@@ -1286,7 +1252,7 @@ public class DeliveryTransferService {
|
||||
&& Objects.equals(item.getInboundCode(), balance.getInboundCode());
|
||||
}
|
||||
|
||||
private DeliveryResponses.PdaTask taskResult(OutboundOrder outbound, OutboundPdaTask task) { return new DeliveryResponses.PdaTask(task.getId(), task.getTaskNo(), task.getStatus(), outbound.getOutboundNo(), outbound.getOutboundType(), task.getPushedAt(), task.getCompletedAt()); }
|
||||
private DeliveryResponses.PdaTask taskResult(OutboundOrder outbound, OutboundPdaTask task) { return new DeliveryResponses.PdaTask(task.getId(), task.getTaskNo(), task.getStatus(), outbound.getOutboundNo(), task.getPushedAt(), task.getCompletedAt(), outbound.getOutboundType()); }
|
||||
private DeliveryResponses.PdaTaskResolution outboundResolution(OutboundOrder outbound, OutboundPdaTask task) {
|
||||
return new DeliveryResponses.PdaTaskResolution("OUTBOUND", task.getTaskNo(), outbound.getOutboundNo(),
|
||||
outbound.getDispatchNo());
|
||||
@@ -1353,75 +1319,51 @@ public class DeliveryTransferService {
|
||||
public DeliveryResponses.DeliveryDetail deliveryDetail(String customerId, String deliveryNo) {
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("Delivery order not found");
|
||||
OutboundOrder sourceOutbound = outboundOrders.selectById(delivery.getOutboundOrderId());
|
||||
if (sourceOutbound != null && customerId.equals(sourceOutbound.getCustomerId())
|
||||
&& ("RETURN_OUTBOUND".equals(sourceOutbound.getOutboundType())
|
||||
|| ("RELABEL_OUTBOUND".equals(sourceOutbound.getOutboundType())
|
||||
&& Objects.toString(delivery.getDeliveryGroupKey(), "").startsWith("RETURN_RELABEL:")))) {
|
||||
List<DeliveryResponses.DeliverySku> cargo = new ArrayList<>();
|
||||
for (OutboundPalletItem item : outboundPallets.findByOutbound(customerId, sourceOutbound.getId())) {
|
||||
if ("RELABEL_OUTBOUND".equals(sourceOutbound.getOutboundType())
|
||||
&& !Objects.equals(delivery.getDestinationWarehouseCode(), item.getDestinationWarehouseCode())) continue;
|
||||
ReturnInventoryCarton carton = returnCartons.findCustomerMark(customerId, item.getCartonMarks());
|
||||
if (carton == null) throw conflict("退货派送单来源箱记录缺失");
|
||||
for (var line : returnCartonItems.listCarton(carton.getId()))
|
||||
cargo.add(new DeliveryResponses.DeliverySku(item.getInboundCode(), delivery.getCustomerName(),
|
||||
carton.getDestinationWarehouseCode(), delivery.getDeliveryAddress(), null, null,
|
||||
line.getSkuCode(), line.getProductNameZh(), 1, null, null, line.getQuantity()));
|
||||
}
|
||||
return new DeliveryResponses.DeliveryDetail(deliveryRow(delivery), cargo,
|
||||
feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId()).stream().map(this::feeRow).toList(),
|
||||
deliveryOperations.findByDelivery(customerId, delivery.getId()).stream().map(this::operationRow).toList(), null);
|
||||
}
|
||||
List<DispatchItem> items = dispatchItems.findByDispatchOrder(customerId, delivery.getDispatchOrderId()).stream()
|
||||
.filter(item -> delivery.getDeliveryGroupKey().equals(deliveryGroupKey(item))).toList();
|
||||
var picked = dispatchPickOrders.deliveryPickedSkus(customerId, delivery.getDispatchOrderId()).stream()
|
||||
.filter(row -> items.stream().anyMatch(item -> Objects.equals(item.getId(), row.dispatchItemId()))).toList();
|
||||
Set<Long> sourceOrderIds = new LinkedHashSet<>();
|
||||
items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).forEach(sourceOrderIds::add);
|
||||
picked.stream().map(com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaPickOrderMapper.DeliveryPickedSku::orderId)
|
||||
.filter(Objects::nonNull).forEach(sourceOrderIds::add);
|
||||
List<Long> orderIds = List.copyOf(sourceOrderIds);
|
||||
List<Long> orderIds = items.stream().filter(item -> !"RETURN_RELABEL".equals(item.getSourceStage()))
|
||||
.map(DispatchItem::getOrderId).filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, LastlegOrder> ordersById = orderIds.isEmpty() ? Map.of() : tailOrders.selectBatchIds(orderIds).stream()
|
||||
.filter(order -> customerId.equals(order.getCustomerId())).collect(java.util.stream.Collectors.toMap(LastlegOrder::getId, order -> order));
|
||||
List<LastlegOrderDetail> details = orderIds.isEmpty() ? List.of() : tailOrderDetails.selectList(
|
||||
new LambdaQueryWrapper<LastlegOrderDetail>().eq(LastlegOrderDetail::getCustomerId, customerId).in(LastlegOrderDetail::getOrderId, orderIds));
|
||||
List<DeliveryResponses.DeliverySku> skus = deliveryCargoSkus(items, picked, ordersById, details);
|
||||
return new DeliveryResponses.DeliveryDetail(deliveryRow(delivery), skus,
|
||||
feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId()).stream().map(this::feeRow).toList(),
|
||||
deliveryOperations.findByDelivery(customerId, delivery.getId()).stream().map(this::operationRow).toList(),
|
||||
feeCoverageEvidence(customerId, delivery.getOutboundNo(), items));
|
||||
}
|
||||
|
||||
private List<DeliveryResponses.DeliverySku> deliveryCargoSkus(List<DispatchItem> items,
|
||||
List<com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaPickOrderMapper.DeliveryPickedSku> picked,
|
||||
Map<Long, LastlegOrder> orders, List<LastlegOrderDetail> details) {
|
||||
List<DeliveryResponses.DeliverySku> result = new ArrayList<>();
|
||||
List<DeliveryResponses.DeliverySku> skus = new ArrayList<>(details.stream()
|
||||
.map(detail -> sku(detail, ordersById.get(detail.getOrderId()))).toList());
|
||||
for (DispatchItem item : items) {
|
||||
var rows = picked.stream().filter(row -> Objects.equals(item.getId(), row.dispatchItemId())).toList();
|
||||
long total = 0;
|
||||
for (var row : rows) {
|
||||
if (row.packages() == null || row.packages() <= 0) throw conflict("当前派送拣货数量无效");
|
||||
total = Math.addExact(total, row.packages());
|
||||
String skuCode = trim(row.skuCode());
|
||||
if ("MIXED".equalsIgnoreCase(skuCode)) skuCode = null;
|
||||
String currentSku = skuCode;
|
||||
var matching = currentSku == null ? List.<LastlegOrderDetail>of() : details.stream()
|
||||
.filter(detail -> Objects.equals(row.orderId(), detail.getOrderId())
|
||||
&& currentSku.equals(detail.getSkuCode())).toList();
|
||||
LastlegOrderDetail metadata = matching.size() == 1 ? matching.get(0) : null;
|
||||
LastlegOrder order = row.orderId() == null ? null : orders.get(row.orderId());
|
||||
result.add(new DeliveryResponses.DeliverySku(order == null ? item.getInboundCode() : order.getInboundCode(),
|
||||
order == null ? null : order.getCustomerName(), firstText(item.getDestinationWarehouseCode(), item.getFbaWarehouseCode()),
|
||||
item.getDeliveryAddress(), metadata == null ? null : metadata.getShipmentNo(),
|
||||
metadata == null ? null : metadata.getFbaTrackingNo(), skuCode,
|
||||
skuCode == null ? null : row.productName(), Math.toIntExact(row.packages()), null, null));
|
||||
}
|
||||
long snapshotPackages = value(item.getPackageCount());
|
||||
if (total > snapshotPackages) throw conflict("当前派送拣货数量超过冻结排车数量");
|
||||
if (total < snapshotPackages) {
|
||||
// Before picking, only the selected dispatch quantity is known; no SKU allocation is invented.
|
||||
LastlegOrder order = item.getOrderId() == null ? null : orders.get(item.getOrderId());
|
||||
result.add(new DeliveryResponses.DeliverySku(item.getInboundCode(), order == null ? null : order.getCustomerName(),
|
||||
firstText(item.getDestinationWarehouseCode(), item.getFbaWarehouseCode()), item.getDeliveryAddress(),
|
||||
null, null, null, null, Math.toIntExact(snapshotPackages - total), null, null));
|
||||
if (!"RETURN_RELABEL".equals(item.getSourceStage())) continue;
|
||||
for (String mark : Objects.toString(item.getCartonMarks(), "").split(",")) {
|
||||
ReturnInventoryCarton carton = returnCartons.findCustomerMark(customerId, mark);
|
||||
if (carton == null || !Objects.equals(delivery.getOverseasWarehouseId(), carton.getOverseasWarehouseId()))
|
||||
throw conflict("退货换标派送单来源箱记录缺失");
|
||||
for (var line : returnCartonItems.listCarton(carton.getId()))
|
||||
skus.add(new DeliveryResponses.DeliverySku(item.getInboundCode(), delivery.getCustomerName(),
|
||||
item.getFbaWarehouseCode(), delivery.getDeliveryAddress(), null, null,
|
||||
line.getSkuCode(), line.getProductNameZh(), 1, null, null, line.getQuantity()));
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private com.baiyue.lastleg.delivery.dto.DeliveryFeeCoverageModels.Evidence feeCoverageEvidence(String customerId, String outboundNo, List<DispatchItem> items) {
|
||||
var outbound = outboundNo == null ? null : outboundOrders.findByNo(customerId, outboundNo);
|
||||
if (outbound == null || outbound.getFeeCoverageJson() == null) return null;
|
||||
try {
|
||||
var evidence = objectMapper.readValue(outbound.getFeeCoverageJson(), com.baiyue.lastleg.delivery.dto.DeliveryFeeCoverageModels.Evidence.class);
|
||||
var ids = items.stream().map(DispatchItem::getId).collect(Collectors.toSet());
|
||||
var cargo = evidence.cargo().stream().filter(row -> ids.contains(row.dispatchItemId())).toList();
|
||||
var inboundIds = cargo.stream().filter(row -> "FIRSTLEG".equals(row.sourceRegion()))
|
||||
.map(com.baiyue.lastleg.delivery.dto.DeliveryFeeCoverageModels.Cargo::sourceInboundId).collect(Collectors.toSet());
|
||||
var references = evidence.references().stream().filter(row -> inboundIds.contains(row.source().sourceInboundId().toString())).toList();
|
||||
return new com.baiyue.lastleg.delivery.dto.DeliveryFeeCoverageModels.Evidence(evidence.cargoHash(), cargo, references, evidence.operatorId(), evidence.confirmedAt());
|
||||
} catch (JsonProcessingException exception) { throw new IllegalStateException("费用覆盖确认无法读取", exception); }
|
||||
return new DeliveryResponses.DeliveryDetail(deliveryRow(delivery), skus,
|
||||
feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId()).stream().map(this::feeRow).toList(),
|
||||
deliveryOperations.findByDelivery(customerId, delivery.getId()).stream().map(this::operationRow).toList(), null);
|
||||
}
|
||||
|
||||
private void bindTailOrders(String customerId, List<DispatchItem> items, String outboundNo, String deliveryNo,
|
||||
@@ -1483,6 +1425,7 @@ public class DeliveryTransferService {
|
||||
allocateMetric(candidate.getSourceVolumeCbm(), candidate.getSourcePackageCount(), available, 4));
|
||||
}
|
||||
private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getSignedPackages(), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt(), value(d.getVersion()), DeliveryStatusPolicy.allowedActions(d.getStatus()), d.getBolNo(), DeliveryDocumentSnapshot.summary(d.getDocumentsJson()), d.getDestinationWarehouseId(), d.getDestinationWarehouseCode(), d.getDestinationWarehouseName(), d.getDestinationType(), d.getDeliveryAddress(), d.getContactName(), d.getContactPhone(), d.getPostalCode()); }
|
||||
private DeliveryResponses.DeliverySku sku(LastlegOrderDetail d, LastlegOrder order) { return new DeliveryResponses.DeliverySku(order == null ? null : order.getInboundCode(), order == null ? null : order.getCustomerName(), d.getDestinationWarehouseCode(), null, d.getShipmentNo(), d.getFbaTrackingNo(), d.getSkuCode(), firstText(d.getProductNameZh(), d.getProductNameEn()), value(d.getPackageCount()), d.getGrossWeightKg(), d.getVolumeCbm()); }
|
||||
private DeliveryResponses.DeliveryFeeRow feeRow(OverseasFeeSource f) { return new DeliveryResponses.DeliveryFeeRow(f.getId(), f.getDirection(), f.getFeeItemCode(), f.getOriginalCurrency(), f.getOriginalAmount(), f.getReviewReason(), f.getReviewStatus(), f.getSourceStatus(), f.getVoidStatus()); }
|
||||
private DeliveryResponses.DeliveryOperationRow operationRow(DeliveryOperation o) { return new DeliveryResponses.DeliveryOperationRow(o.getId(), o.getCreatedAt(), o.getOperationNode(), firstText(o.getOperatorName(), o.getOperatorId()), o.getResult()); }
|
||||
private void recordOperation(String customerId, Long deliveryId, String operatorId, String node, String result, LocalDateTime now) { DeliveryOperation operation = new DeliveryOperation(); operation.setCustomerId(customerId); operation.setDeliveryOrderId(deliveryId); operation.setOperationNode(node); operation.setOperatorId(operatorId); operation.setOperatorName(operatorId); operation.setResult(result); operation.setCreatedAt(now); deliveryOperations.insert(operation); }
|
||||
@@ -1539,11 +1482,6 @@ public class DeliveryTransferService {
|
||||
java.util.function.Function<T, BigDecimal> getter) {
|
||||
return items.stream().map(getter).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
private static <T> BigDecimal sumKnownPhysicalWeight(List<T> items,
|
||||
java.util.function.Function<T, BigDecimal> getter) {
|
||||
if (items.isEmpty() || items.stream().map(getter).anyMatch(weight -> weight == null || weight.signum() <= 0)) return null;
|
||||
return items.stream().map(getter).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
private static BigDecimal allocateMetric(BigDecimal sourceValue, Long sourcePackages,
|
||||
long selectedPackages, int scale) {
|
||||
if (sourceValue == null || sourceValue.signum() == 0 || sourcePackages == null
|
||||
|
||||
+1
-4
@@ -77,10 +77,7 @@ public class CargoDiscardController {
|
||||
private String scope(AuthenticatedUser user, String requested) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requested == null || requested.isBlank() || "0".equals(requested)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requested.trim();
|
||||
return requested == null || requested.isBlank() || "0".equals(requested) ? null : requested.trim();
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public final class CargoDiscardResponses {
|
||||
List<CandidateSku> skus) {}
|
||||
public record CandidateSku(String skuCode, String productName, long availablePackages) {}
|
||||
|
||||
public record Summary(Long id, String discardNo, String inboundCodes,
|
||||
public record Summary(Long id, String customerId, String discardNo, String inboundCodes,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
int totalPackages, BigDecimal totalFee, String currency, String status,
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt) {}
|
||||
|
||||
+4
-33
@@ -51,7 +51,7 @@ import java.util.Set;
|
||||
@Service
|
||||
public class CargoDiscardService {
|
||||
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
|
||||
public static final String APPROVAL_PERMISSION = "lastleg:cargo-discard:approve";
|
||||
public static final String APPROVAL_PERMISSION = "platform:overseas-warehouse:discard:approve";
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
public static final String APPROVED = "APPROVED";
|
||||
public static final String REJECTED = "REJECTED";
|
||||
@@ -171,36 +171,7 @@ public class CargoDiscardService {
|
||||
application.setCreatedAt(now); application.setUpdatedAt(now);
|
||||
applications.insert(application);
|
||||
|
||||
// A discard request is also a customer-visible tail-leg order. Keep the
|
||||
// application as the workflow record and link the order through its
|
||||
// immutable source id so cancellation/status changes stay traceable.
|
||||
LastlegOrder tailOrder = new LastlegOrder();
|
||||
tailOrder.setId(IdWorker.getId());
|
||||
tailOrder.setOrderNo(application.getDiscardNo());
|
||||
tailOrder.setCustomerId(customerId);
|
||||
tailOrder.setOrderType("CARGO_DISCARD");
|
||||
tailOrder.setInboundCode(application.getInboundCodes());
|
||||
tailOrder.setInboundOrderNo(application.getDiscardNo());
|
||||
tailOrder.setSourceRegion("LASTLEG_DISCARD");
|
||||
tailOrder.setSourceInboundId("CARGO_DISCARD:" + application.getId());
|
||||
tailOrder.setSourceOrderId(String.valueOf(application.getId()));
|
||||
tailOrder.setSourceOrderNo(application.getDiscardNo());
|
||||
tailOrder.setOverseasWarehouseId(warehouseId);
|
||||
tailOrder.setOverseasWarehouseName(warehouseName);
|
||||
tailOrder.setPackageCount(total);
|
||||
tailOrder.setCurrentStatus(PENDING_REVIEW);
|
||||
tailOrder.setVersion(0);
|
||||
tailOrder.setCreatedAt(now);
|
||||
tailOrder.setUpdatedAt(now);
|
||||
tailOrder.setSourceEventId("CARGO_DISCARD:" + application.getId());
|
||||
tailOrder.setSourceLoadingPlanId("CARGO_DISCARD");
|
||||
tailOrder.setSourcePayloadHash(requestHash);
|
||||
orders.insert(tailOrder);
|
||||
LastlegOrderStatusHistory initialOrderHistory = new LastlegOrderStatusHistory();
|
||||
initialOrderHistory.setId(IdWorker.getId()); initialOrderHistory.setCustomerId(customerId); initialOrderHistory.setOrderId(tailOrder.getId());
|
||||
initialOrderHistory.setFromStatus(null); initialOrderHistory.setToStatus(PENDING_REVIEW);
|
||||
initialOrderHistory.setEventId("CARGO_DISCARD_CREATE:" + application.getId()); initialOrderHistory.setRemark(trim(request.remark())); initialOrderHistory.setCreatedAt(now);
|
||||
orderHistories.insert(initialOrderHistory);
|
||||
// 废弃申请独立承载废弃流程,不创建尾程订单。
|
||||
|
||||
Set<Long> reservedReturnCartons = new LinkedHashSet<>();
|
||||
for (CargoDiscardRequests.Item input : request.items()) {
|
||||
@@ -218,7 +189,7 @@ public class CargoDiscardService {
|
||||
public PageResult<CargoDiscardResponses.Summary> page(String customerId, CargoDiscardRequests.Query query) {
|
||||
validateQuery(query);
|
||||
LambdaQueryWrapper<CargoDiscardApplication> wrapper = new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||
.eq(CargoDiscardApplication::getCustomerId, customerId)
|
||||
.eq(StringUtils.hasText(customerId), CargoDiscardApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.discardNo()), CargoDiscardApplication::getDiscardNo, query.discardNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), CargoDiscardApplication::getInboundCodes, query.inboundCode())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), CargoDiscardApplication::getOverseasWarehouseId,
|
||||
@@ -432,7 +403,7 @@ public class CargoDiscardService {
|
||||
}
|
||||
|
||||
private CargoDiscardResponses.Summary summary(CargoDiscardApplication value) {
|
||||
return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getInboundCodes(),
|
||||
return new CargoDiscardResponses.Summary(value.getId(), value.getCustomerId(), value.getDiscardNo(), value.getInboundCodes(),
|
||||
value.getOverseasWarehouseId(), value.getOverseasWarehouseName(), value.getTotalPackages(),
|
||||
value.getTotalFee(), value.getCurrency(), value.getStatus(), value.getCreatedBy(), value.getCreatedAt(),
|
||||
value.getReviewedAt());
|
||||
|
||||
+21
@@ -258,6 +258,27 @@ public interface DispatchCandidateMapper {
|
||||
@Param("orderNo") String orderNo,
|
||||
@Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("<script>SELECT a.customer_id AS customerId,a.lastleg_order_id AS orderId,a.overseas_warehouse_id AS overseasWarehouseId," +
|
||||
"MIN(ri.source_warehouse_name) AS overseasWarehouseName,a.relabel_no AS orderNo,a.relabel_no AS orderRefs," +
|
||||
"GROUP_CONCAT(DISTINCT c.source_return_no ORDER BY c.source_return_no SEPARATOR ',') AS inboundCode," +
|
||||
"c.pallet_code AS palletNo,COUNT(DISTINCT c.id) AS packageCount,'RELABEL' AS sourceStage,1 AS returnCartonSource," +
|
||||
"GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') AS relabelCartonMarks," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode " +
|
||||
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
|
||||
"JOIN ll_return_inventory_carton c ON c.id=ri.return_carton_id AND c.customer_id=a.customer_id " +
|
||||
"AND c.relabel_application_id=a.id AND c.overseas_warehouse_id=a.overseas_warehouse_id " +
|
||||
"WHERE a.delivery_method='TRUCK' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"AND c.status='RELABEL_PENDING' AND c.pallet_code IS NOT NULL " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND a.customer_id=#{customerId} </if>" +
|
||||
"<if test='relabelNo != null and relabelNo != \"\"'>AND a.relabel_no=#{relabelNo} </if>" +
|
||||
"<if test='orderNo != null and orderNo != \"\"'>AND a.relabel_no LIKE CONCAT('%',#{orderNo},'%') </if>" +
|
||||
"AND NOT EXISTS(SELECT 1 FROM ll_dispatch_item di JOIN ll_dispatch_order d ON d.id=di.dispatch_order_id " +
|
||||
"AND d.customer_id=di.customer_id WHERE di.customer_id=a.customer_id AND di.source_business_no=a.relabel_no AND d.status!='CANCELLED') " +
|
||||
"GROUP BY a.id,a.customer_id,a.lastleg_order_id,a.overseas_warehouse_id,a.relabel_no,c.pallet_code</script>")
|
||||
List<DispatchPalletCandidate> nativeReturnRelabelAvailable(@Param("customerId") String customerId,
|
||||
@Param("orderNo") String orderNo, @Param("relabelNo") String relabelNo);
|
||||
|
||||
@org.apache.ibatis.annotations.Update("UPDATE ll_relabel_application SET fulfillment_status='PRE_DISPATCH_CREATED',updated_at=CURRENT_TIMESTAMP(3) " +
|
||||
"WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND fulfillment_status='WAIT_PRE_DISPATCH'")
|
||||
int markRelabelPreDispatched(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ public class DispatchPalletCandidate {
|
||||
private String contactPhone;
|
||||
private Long returnReceiptId;
|
||||
private String returnReceiptNo;
|
||||
private boolean returnCartonSource;
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("sourceType")
|
||||
public String sourceType() { return returnReceiptId == null ? "INBOUND_PLAN" : "RETURN_RECEIPT"; }
|
||||
|
||||
|
||||
+6
-2
@@ -8,8 +8,12 @@ import jakarta.validation.constraints.Size;
|
||||
|
||||
public final class DispatchPdaRequests {
|
||||
private DispatchPdaRequests() {}
|
||||
public record Pick(@NotNull Long positionId, @Positive long packages, @PositiveOrZero long productQuantity,
|
||||
@NotBlank String idempotencyKey, String remark) {}
|
||||
public record Pick(Long positionId, @Positive long packages, @PositiveOrZero long productQuantity,
|
||||
@NotBlank String idempotencyKey, String remark, @Size(max = 255) String cartonMark) {
|
||||
public Pick(Long positionId, long packages, long productQuantity, String idempotencyKey, String remark) {
|
||||
this(positionId, packages, productQuantity, idempotencyKey, remark, null);
|
||||
}
|
||||
}
|
||||
public record Depart(@NotBlank String idempotencyKey, String remark) {}
|
||||
public record CompletePicking(@NotBlank String idempotencyKey) {}
|
||||
}
|
||||
|
||||
+9
-1
@@ -16,7 +16,15 @@ public final class DispatchPdaResponses {
|
||||
public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel,
|
||||
int scheduledPackages,
|
||||
long pickedPackages, List<Position> positions) {}
|
||||
long pickedPackages, List<Position> positions, List<ReturnCarton> returnCartons) {
|
||||
public Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel,
|
||||
int scheduledPackages, long pickedPackages, List<Position> positions) {
|
||||
this(dispatchItemId, palletNo, destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode,
|
||||
warehouseRouteLabel, scheduledPackages, pickedPackages, positions, List.of());
|
||||
}
|
||||
}
|
||||
public record ReturnCarton(Long id, String cartonMark, String status, long productQuantity) {}
|
||||
public record Position(Long id, Long balanceId, long availablePackages, long allocatedPackages,
|
||||
long pickedPackages) {}
|
||||
public record Picked(Long dispatchItemId, Long positionId, long packages, long pickedPackages,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ public class DispatchPdaPick {
|
||||
private Long taskId;
|
||||
private Long dispatchItemId;
|
||||
private Long inventoryPositionId;
|
||||
private Long returnCartonId;
|
||||
private Long packages;
|
||||
private Long productQuantity;
|
||||
private String idempotencyKey;
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ public class DispatchPdaPickOrder {
|
||||
private Long pickId;
|
||||
private Long receiptId;
|
||||
private Long returnReceiptId;
|
||||
private Long returnCartonId;
|
||||
private Long orderId;
|
||||
private Long packages;
|
||||
private Long productQuantity;
|
||||
|
||||
+25
-5
@@ -54,6 +54,7 @@ public class DispatchPdaWorkflowService {
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Resource private DispatchPickOrderAttributionService pickAttribution;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private com.baiyue.lastleg.dispatch.service.ReturnRelabelDispatchStock returnRelabelStock;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<DispatchPdaResponses.Task> list(String warehouseId, String status) {
|
||||
@@ -115,13 +116,23 @@ public class DispatchPdaWorkflowService {
|
||||
throw notFound("Dispatch pallet item not found");
|
||||
}
|
||||
if (!warehouseId.equals(item.getOverseasWarehouseId())) throw conflict("Pallet does not belong to selected warehouse");
|
||||
InventoryPosition position = positions.selectById(request.positionId());
|
||||
if (position == null || !customerId.equals(position.getCustomerId())
|
||||
|| item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId())) {
|
||||
boolean nativeReturn = "RETURN_RELABEL".equals(item.getSourceStage());
|
||||
if (!nativeReturn && request.positionId() == null) throw conflict("普通入仓拣货必须选择库存仓位");
|
||||
if (nativeReturn && request.positionId() != null) throw conflict("退货箱拣货请扫描来源箱唛,不使用普通库存仓位");
|
||||
InventoryPosition position = nativeReturn ? null : positions.selectById(request.positionId());
|
||||
if (!nativeReturn && (position == null || !customerId.equals(position.getCustomerId())
|
||||
|| item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId()))) {
|
||||
throw conflict("Inventory position does not belong to this dispatch pallet");
|
||||
}
|
||||
long picked = picks.totalItem(customerId, task.getId(), itemId);
|
||||
if (picked + request.packages() > item.getPackageCount()) throw conflict("Picked packages exceed dispatch pallet packages");
|
||||
long pickId = IdWorker.getId();
|
||||
Long returnCartonId = null;
|
||||
if (nativeReturn) {
|
||||
pickAttribution.prepareNativeReturn(customerId, item.getOrderId());
|
||||
returnCartonId = returnRelabelStock.pick(item, request.cartonMark(), request.packages(), request.productQuantity(),
|
||||
pickId, order.getDispatchNo()).getId();
|
||||
} else {
|
||||
pickAttribution.preparePicking(customerId, request.positionId(),
|
||||
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null);
|
||||
String keyBase = "DISPATCH:" + order.getDispatchNo() + ":" + itemId + ":" + request.idempotencyKey();
|
||||
@@ -142,12 +153,14 @@ public class DispatchPdaWorkflowService {
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":SHIP", request.remark()));
|
||||
}
|
||||
DispatchPdaPick pick = new DispatchPdaPick();
|
||||
pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId());
|
||||
pick.setId(pickId); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setReturnCartonId(returnCartonId);
|
||||
pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages());
|
||||
pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey());
|
||||
pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick);
|
||||
pickAttribution.attribute(customerId, pick,
|
||||
if (nativeReturn) pickAttribution.attributeNativeReturn(customerId, pick, item.getOrderId());
|
||||
else pickAttribution.attribute(customerId, pick,
|
||||
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null, item.getReturnReceiptId());
|
||||
long total = picked + request.packages();
|
||||
order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages()));
|
||||
@@ -289,6 +302,13 @@ public class DispatchPdaWorkflowService {
|
||||
private DispatchPdaResponses.Task task(String customerId, DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source) {
|
||||
List<DispatchPdaResponses.Item> values = new ArrayList<>();
|
||||
for (DispatchItem item : source) {
|
||||
if ("RETURN_RELABEL".equals(item.getSourceStage())) {
|
||||
values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(),
|
||||
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
|
||||
warehouseRouteLabel(item.getFbaWarehouseCode(), item.getDestinationWarehouseCode()), item.getPackageCount(),
|
||||
picks.totalItem(customerId, task.getId(), item.getId()), List.of(), returnRelabelStock.detail(item)));
|
||||
continue;
|
||||
}
|
||||
List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of()
|
||||
: positions.listPallet(customerId, item.getWarehousePalletId());
|
||||
boolean relabel = "RELABEL_ORDER".equals(item.getSourceBusinessType());
|
||||
|
||||
+18
@@ -162,6 +162,24 @@ public class DispatchPickOrderAttributionService {
|
||||
}
|
||||
|
||||
public record OrderPackages(Long orderId, long packages) {}
|
||||
/** Use the same order-before-stock lock order as ordinary dispatch picks. */
|
||||
@org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void prepareNativeReturn(String customerId, Long orderId) {
|
||||
LastlegOrder order = orderId == null ? null : orders.lockById(customerId, orderId);
|
||||
if (order == null || Boolean.TRUE.equals(order.getDetained()))
|
||||
throw conflict("退货换标履约订单缺失或暂扣");
|
||||
}
|
||||
/** Native return boxes have their own stock ledger, but fulfill this relabel tail order. */
|
||||
public void attributeNativeReturn(String customerId, DispatchPdaPick pick, Long orderId) {
|
||||
LastlegOrder order = orderId == null ? null : orders.lockById(customerId, orderId);
|
||||
if (order == null || Boolean.TRUE.equals(order.getDetained())) throw conflict("退货换标履约订单缺失或暂扣");
|
||||
DispatchPdaPickOrder row = new DispatchPdaPickOrder();
|
||||
row.setId(IdWorker.getId()); row.setCustomerId(customerId); row.setPickId(pick.getId()); row.setOrderId(orderId);
|
||||
row.setReturnCartonId(pick.getReturnCartonId());
|
||||
row.setPackages(pick.getPackages()); row.setProductQuantity(pick.getProductQuantity()); row.setCreatedAt(pick.getPickedAt());
|
||||
if (attributions.insert(row) != 1 || orders.increasePicked(customerId, orderId, pick.getPackages()) != 1)
|
||||
throw conflict("退货换标排车拣货归属或箱数不一致");
|
||||
}
|
||||
private static long value(Long value) { return value == null ? 0 : value; }
|
||||
private static long value(Integer value) { return value == null ? 0 : value; }
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
|
||||
+29
-4
@@ -78,6 +78,7 @@ public class DispatchService {
|
||||
@Resource private com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard;
|
||||
@Resource private ReturnRelabelDispatchStock returnRelabelStock;
|
||||
|
||||
public String resolveCustomerScopeForPlans(List<Long> planIds) {
|
||||
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
|
||||
@@ -122,7 +123,9 @@ public class DispatchService {
|
||||
List<DispatchPalletCandidate> result = "RELABEL".equals(sourceStage) ? List.of() : candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||
trimToNull(sourceStage), trimToNull(draftToken));
|
||||
if (!StringUtils.hasText(sourceStage) || Set.of("RECEIVED", "RELABEL").contains(sourceStage)) {
|
||||
List<DispatchPalletCandidate> relabel = candidates.relabelAvailable(null, trimToNull(orderNo), null);
|
||||
List<DispatchPalletCandidate> relabel = java.util.stream.Stream.concat(
|
||||
candidates.relabelAvailable(null, trimToNull(orderNo), null).stream(),
|
||||
returnRelabelStock.candidates(null, trimToNull(orderNo), null).stream()).toList();
|
||||
if (relabel != null && !relabel.isEmpty()) {
|
||||
result = java.util.stream.Stream.concat(result.stream(), relabel.stream().filter(item ->
|
||||
!StringUtils.hasText(fbaWarehouseCode) || Objects.toString(item.fbaWarehouseCode(), "")
|
||||
@@ -567,15 +570,22 @@ public class DispatchService {
|
||||
|
||||
@Transactional
|
||||
private DispatchResponse preArrangeRelabel(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
||||
RelabelApplication application = relabelApplications.findNo(customerId, request.relabelNo());
|
||||
RelabelApplication application = relabelApplications.lockNo(customerId, request.relabelNo());
|
||||
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车");
|
||||
}
|
||||
relabelBindingGuard.requireBound(application);
|
||||
List<DispatchPalletCandidate> selected = candidates.relabelAvailable(customerId, null, request.relabelNo());
|
||||
if (!"TRUCK".equals(application.getDeliveryMethod()) || !"WAIT_PRE_DISPATCH".equals(application.getFulfillmentStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标单不是待排车状态");
|
||||
List<DispatchPalletCandidate> selected = java.util.stream.Stream.concat(
|
||||
candidates.relabelAvailable(customerId, null, request.relabelNo()).stream(),
|
||||
returnRelabelStock.candidates(customerId, null, request.relabelNo()).stream()).toList();
|
||||
Set<Long> requested = request.planPalletIds().stream().map(Long::valueOf).collect(Collectors.toSet());
|
||||
if (selected.stream().anyMatch(DispatchPalletCandidate::returnCartonSource)
|
||||
&& !requested.equals(selected.stream().map(DispatchPalletCandidate::warehousePalletId).collect(Collectors.toSet())))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货换标排车须选择本申请全部已绑定托盘");
|
||||
selected = selected.stream().filter(c -> requested.contains(c.warehousePalletId())).toList();
|
||||
if (selected.isEmpty() || selected.size() != requested.size()) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单托盘候选已变化,请刷新后重试");
|
||||
if (selected.isEmpty() || selected.stream().map(DispatchPalletCandidate::warehousePalletId).distinct().count() != requested.size()) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单托盘候选已变化,请刷新后重试");
|
||||
String warehouse = selected.get(0).overseasWarehouseId();
|
||||
if (selected.stream().anyMatch(c -> !Objects.equals(warehouse, c.overseasWarehouseId()))) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单只能选择同一海外仓");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -584,6 +594,12 @@ public class DispatchService {
|
||||
dispatch.setRoute(candidateRoute(selected)); dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo())); dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus())); dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus())); dispatch.setStatus(PENDING_DEPARTURE); dispatch.setItemCount(selected.size()); dispatch.setPalletCount(selected.size()); dispatch.setPackageCount(selected.stream().map(DispatchPalletCandidate::packageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); dispatch.setPickedPackageCount(0); dispatch.setShippedPackageCount(0); dispatch.setCreatedBy(trimToNull(operatorId)); dispatch.setCreatedAt(now); dispatch.setUpdatedAt(now); orders.insert(dispatch);
|
||||
for (DispatchPalletCandidate candidate : selected) { DispatchItem item = new DispatchItem(); item.setId(IdWorker.getId()); item.setDispatchOrderId(dispatch.getId()); item.setCustomerId(customerId); item.setWarehousePalletId(candidate.warehousePalletId()); item.setOrderId(candidate.orderId()); item.setOverseasWarehouseId(candidate.overseasWarehouseId()); item.setOverseasWarehouseName(candidate.overseasWarehouseName()); item.setOrderNo(candidate.orderNo()); item.setOrderRefs(candidate.orderRefs()); item.setInboundCode(candidate.inboundCode()); item.setPalletNo(candidate.palletNo()); item.setDestinationWarehouseCode(candidate.destinationWarehouseCode()); item.setDestinationWarehouseName(candidate.destinationWarehouseName()); item.setFbaWarehouseCode(candidate.fbaWarehouseCode()); item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0)); item.setGrossWeightKg(candidate.grossWeightKg()); item.setVolumeWeightKg(Objects.requireNonNullElse(candidate.volumeWeightKg(), BigDecimal.ZERO)); item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO)); item.setSourceStage("RELABEL"); item.setCartonMarks(candidate.relabelCartonMarks()); item.setSourceBusinessType("RELABEL_ORDER"); item.setSourceBusinessNo(request.relabelNo()); item.setCreatedAt(now); items.insert(item); }
|
||||
DispatchPdaTask task = new DispatchPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(dispatch.getId()); task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING"); task.setWarehouseId(warehouse); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task);
|
||||
for (DispatchItem item : items.findByDispatchOrder(customerId, dispatch.getId())) {
|
||||
if (selected.stream().anyMatch(c -> c.returnCartonSource() && Objects.equals(c.warehousePalletId(), item.getWarehousePalletId())
|
||||
&& Objects.equals(c.relabelCartonMarks(), item.getCartonMarks()))) {
|
||||
item.setSourceStage("RETURN_RELABEL"); items.updateById(item); returnRelabelStock.reserve(item);
|
||||
}
|
||||
}
|
||||
if (candidates.markRelabelPreDispatched(customerId, request.relabelNo()) != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单已被其他排车占用");
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
@@ -685,6 +701,8 @@ public class DispatchService {
|
||||
requirePalletEditingAllowed(dispatch);
|
||||
DispatchItem item = items.lockById(dispatch.getCustomerId(), dispatch.getId(), itemId);
|
||||
if (item == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch pallet not found");
|
||||
if ("RETURN_RELABEL".equals(item.getSourceStage()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货换标按整单排车,请取消整张排车单后重新排车");
|
||||
|
||||
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getCustomerId, dispatch.getCustomerId())
|
||||
@@ -734,10 +752,17 @@ public class DispatchService {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Only a pending dispatch can be cancelled");
|
||||
}
|
||||
String customerId = dispatch.getCustomerId();
|
||||
if (Objects.requireNonNullElse(dispatch.getPickedPackageCount(), 0) > 0)
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "排车已开始拣货,不能取消并释放已出库箱");
|
||||
if (outboundOrders.findByDispatch(customerId, dispatch.getId()) != null)
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "已生成出库单,请先取消对应出库单再取消排车");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
dispatch.setStatus(CANCELLED);
|
||||
dispatch.setUpdatedAt(now);
|
||||
orders.updateById(dispatch);
|
||||
for (DispatchItem item : items.findByDispatchOrder(customerId, dispatch.getId())) {
|
||||
if ("RETURN_RELABEL".equals(item.getSourceStage())) returnRelabelStock.release(item);
|
||||
}
|
||||
items.findByDispatchOrder(customerId, dispatch.getId()).stream()
|
||||
.filter(item -> "RELABEL_ORDER".equals(item.getSourceBusinessType()))
|
||||
.map(DispatchItem::getSourceBusinessNo).filter(StringUtils::hasText).distinct()
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
|
||||
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** Native return stock stays in its own ledger; no synthetic InventoryPosition rows. */
|
||||
@Service
|
||||
public class ReturnRelabelDispatchStock {
|
||||
@Resource private DispatchCandidateMapper candidates;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private PalletPhysicalGateway physical;
|
||||
@Resource private ReturnInventoryService inventory;
|
||||
|
||||
public List<DispatchPalletCandidate> candidates(String customer, String orderNo, String relabelNo) {
|
||||
List<DispatchPalletCandidate> result = new ArrayList<>();
|
||||
for (var candidate : candidates.nativeReturnRelabelAvailable(customer, orderNo, relabelNo)) {
|
||||
var pallet = physical.resolve(candidate.palletNo());
|
||||
if (pallet == null || !String.valueOf(pallet.warehouseId()).equals(candidate.overseasWarehouseId())
|
||||
|| !Set.of("IDLE", "OCCUPIED").contains(pallet.status())) continue;
|
||||
candidate.warehousePalletId(pallet.palletId());
|
||||
result.add(candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void reserve(DispatchItem item) {
|
||||
for (String mark : marks(item).stream().sorted().toList()) {
|
||||
var carton = require(item, mark, "RELABEL_PENDING");
|
||||
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(LocalDateTime.now());
|
||||
if (cartons.updateById(carton) != 1) throw conflict("退货箱排车预占失败");
|
||||
}
|
||||
}
|
||||
|
||||
public void release(DispatchItem item) {
|
||||
for (String mark : marks(item).stream().sorted().toList()) {
|
||||
var carton = require(item, mark, "OUTBOUND_PENDING");
|
||||
carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(LocalDateTime.now());
|
||||
if (cartons.updateById(carton) != 1) throw conflict("退货箱排车取消释放失败");
|
||||
}
|
||||
}
|
||||
|
||||
public ReturnInventoryCarton pick(DispatchItem item, String mark, long packages, long quantity, long pickId, String dispatchNo) {
|
||||
if (packages != 1 || !marks(item).contains(mark)) throw conflict("请扫描本排车的来源箱唛,每次拣货一箱");
|
||||
var carton = require(item, mark, "OUTBOUND_PENDING");
|
||||
if (quantity != carton.getTotalQuantity()) throw conflict("退货箱 SKU 总数量已变化,请刷新后重试");
|
||||
inventory.shipCarton(item.getCustomerId(), item.getOverseasWarehouseId(), carton.getSourceReturnNo(),
|
||||
carton.getCartonMark(), dispatchNo, pickId);
|
||||
return carton;
|
||||
}
|
||||
|
||||
public List<DispatchPdaResponses.ReturnCarton> detail(DispatchItem item) {
|
||||
return marks(item).stream().map(mark -> {
|
||||
var carton = cartons.findCustomerMark(item.getCustomerId(), mark);
|
||||
if (carton == null || !item.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId()))
|
||||
throw conflict("排车来源退货箱缺失");
|
||||
return new DispatchPdaResponses.ReturnCarton(carton.getId(), mark, carton.getStatus(), carton.getTotalQuantity());
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private ReturnInventoryCarton require(DispatchItem item, String mark, String status) {
|
||||
var carton = cartons.lockMark(mark);
|
||||
if (carton == null || !item.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !item.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|
||||
|| !Objects.equals(item.getPalletNo(), carton.getPalletCode())
|
||||
|| !Objects.equals(item.getSourceBusinessNo(), carton.getRelabelNo()) || !status.equals(carton.getStatus()))
|
||||
throw conflict("退货箱已被占用或来源、客户、仓库、托盘不一致");
|
||||
return carton;
|
||||
}
|
||||
private Set<String> marks(DispatchItem item) {
|
||||
return new LinkedHashSet<>(Arrays.asList(Objects.toString(item.getCartonMarks(), "").split(",")));
|
||||
}
|
||||
private ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
+6
@@ -26,6 +26,12 @@ public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> {
|
||||
List<InventoryPosition> listPallet(@Param("customerId") String customerId,
|
||||
@Param("palletId") Long palletId);
|
||||
|
||||
/** Physical carriers can contain stock owned by more than one customer. */
|
||||
@Select("SELECT COUNT(*) FROM ll_inventory_position WHERE overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND pallet_id=#{palletId} AND (available_packages>0 OR hold_packages>0 OR allocated_packages>0 " +
|
||||
"OR picked_packages>0 OR receiving_packages>0)")
|
||||
long activePhysicalPallet(@Param("warehouseId") String warehouseId, @Param("palletId") Long palletId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId} " +
|
||||
"AND pallet_id=#{palletId} AND batch_no=#{batchNo} LIMIT 1 FOR UPDATE")
|
||||
InventoryPosition lockPosition(@Param("customerId") String customerId,
|
||||
|
||||
+17
-8
@@ -108,7 +108,8 @@ public class PdaPalletOperationService {
|
||||
PalletPhysicalGateway.PalletReference pallet = physical.resolve(carton.getPalletCode());
|
||||
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId())
|
||||
throw conflict("退货换标来源托盘不属于当前仓库");
|
||||
long quantity = value(carton.getTotalQuantity());
|
||||
long quantity = entry.getValue().stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(item.getOperationType()))
|
||||
? value(carton.getTotalQuantity()) : 1;
|
||||
result.add(new PalletOperationResponses.RelabelSource(carton.getId(), 0L,
|
||||
pallet.palletId(), pallet.palletCode(), null, carton.getSourceBatchNo(), quantity, quantity,
|
||||
entry.getValue().stream().map(RelabelItem::getCartonMark).filter(Objects::nonNull).toList()));
|
||||
@@ -159,11 +160,12 @@ public class PdaPalletOperationService {
|
||||
String fbaValue = destination == null ? pallet.fbaWarehouseCode() : destination;
|
||||
String fba = HtmlUtils.htmlEscape(fbaValue == null ? "" : fbaValue);
|
||||
if (fba == null || fba.isBlank()) fba = "待绑定";
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code
|
||||
+ "</title><style>@page{size:100mm 70mm;margin:4mm}body{font-family:Arial,sans-serif;text-align:center;margin:0}"
|
||||
+ "h1{font-size:24px;margin:8mm 0 4mm;letter-spacing:0}.code{font-size:18px;font-weight:700;word-break:break-all}"
|
||||
+ ".ref{font-size:12px;margin-top:6mm}</style></head><body><h1>托盘标签</h1><div class=\"code\">"
|
||||
+ code + "</div><div class=\"ref\">换标单:" + order + "<br>FBA目的仓:" + fba + "</div></body></html>";
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>托盘标签 " + code
|
||||
+ "</title><style>@page{size:100mm 70mm;margin:0}*{box-sizing:border-box}body{width:100mm;height:70mm;margin:0;padding:6mm 8mm;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;text-align:center;color:#111}"
|
||||
+ ".code{max-width:84mm;font-size:14px;font-weight:700;line-height:1.15;overflow-wrap:anywhere}.ref{max-width:84mm;font-size:9px;line-height:1.35;margin-top:3mm;overflow-wrap:anywhere}</style></head><body><div class=\"code\">"
|
||||
+ code + "</div><div style=\"width:76mm;margin-top:3mm\">"
|
||||
+ com.baiyue.lastleg.returnreceipt.service.ReturnCartonBarcode.svg(pallet.palletCode())
|
||||
+ "</div><div class=\"ref\">" + order + "<br>" + fba + "</div></body></html>";
|
||||
return new PalletOperationResponses.PrintablePallet(pallet.palletId(), pallet.palletCode(), pallet.version(),
|
||||
relabelNo, html);
|
||||
}
|
||||
@@ -335,7 +337,8 @@ public class PdaPalletOperationService {
|
||||
String requestHash = hash(request);
|
||||
PdaPalletOperation operation = begin(identity, "RELABEL_PALLET_DISASSEMBLE", key, requestHash);
|
||||
if ("COMPLETED".equals(operation.getStatus())) return read(operation).asReplay();
|
||||
physical.apply(identity.overseasWarehouseId(), target, "OCCUPIED", target.locationId(), key, "退货换标组托");
|
||||
var transition = physical.apply(identity.overseasWarehouseId(), target, "OCCUPIED", target.locationId(), key, "退货换标组托");
|
||||
try {
|
||||
return transaction.execute(status -> {
|
||||
for (int index = 0; index < cartons.size(); index++) {
|
||||
RelabelItem item = selected.get(index);
|
||||
@@ -345,7 +348,8 @@ public class PdaPalletOperationService {
|
||||
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|
||||
|| !"RELABEL_PENDING".equals(carton.getStatus()))
|
||||
throw conflict("退货换标来源库存已变化,请刷新后重试");
|
||||
long move = value(item.getNotifiedPackageCount());
|
||||
long move = "RETURN_BULK_SKU_LABEL".equals(item.getOperationType())
|
||||
? value(item.getNotifiedPackageCount()) : value(carton.getTotalQuantity());
|
||||
if (move <= 0 || move > value(carton.getTotalQuantity())) throw conflict("来源托盘可拆数量不足");
|
||||
if (move < value(carton.getTotalQuantity())) {
|
||||
splitReturnRelabelCarton(application, item, carton, target, move);
|
||||
@@ -367,6 +371,11 @@ public class PdaPalletOperationService {
|
||||
operation.setUpdatedAt(result.completedAt()); mapper.updateById(operation);
|
||||
return result;
|
||||
});
|
||||
} catch (RuntimeException failure) {
|
||||
try { physical.compensate(identity.overseasWarehouseId(), transition); }
|
||||
catch (RuntimeException compensationFailure) { failure.addSuppressed(compensationFailure); }
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
/** Keep the unlabelled remainder on its original pallet; move only the confirmed SKU pieces. */
|
||||
|
||||
+52
-5
@@ -100,6 +100,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private OutboundOrderMapper outboundOrders;
|
||||
@Resource private OutboundPalletItemMapper outboundItems;
|
||||
@Resource private OutboundPdaTaskMapper outboundTasks;
|
||||
@Resource private com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper deliveryOrders;
|
||||
@Resource private OverseasFeeSourceService feeSources;
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferRelabelCompletionService transferRelabelCompletion;
|
||||
@@ -264,6 +265,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
"INBOUND", null, null, List.of(), 0);
|
||||
}).filter(candidate -> candidate.availablePackages() > 0).toList());
|
||||
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, operationWarehouseId.trim())) {
|
||||
if ("PARCEL".equals(carton.getSourceUnitType())) continue;
|
||||
String keyword = trim(inboundCode);
|
||||
if (StringUtils.hasText(keyword)
|
||||
&& !containsIgnoreCase(carton.getCartonMark(), keyword)
|
||||
@@ -390,6 +392,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
|
||||
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
|
||||
}
|
||||
if (returnCartonItems.lockCarton(carton.getId()).stream().anyMatch(line -> value(line.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("退货箱SKU已被合箱计划锁定");
|
||||
if (Set.of("CARTON_LABEL", "CARTON_AND_SKU").contains(item.operationType())
|
||||
&& (item.totalPackageCount() != 1 || item.notifiedPackageCount() != 1))
|
||||
throw conflict("退货箱唛换标按真实箱计数,每个来源箱只能填写一箱");
|
||||
long previouslyRelabelled = returnCartons.relabelledQuantity(carton.getId(), item.skuCode());
|
||||
long requestedPackages = selectedReturnPackages.merge(carton.getId(),
|
||||
(long) item.notifiedPackageCount(), Math::addExact);
|
||||
@@ -910,8 +917,13 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
carton.setRelabelStatus("COMPLETED"); carton.setRelabelCompletedAt(now);
|
||||
// 换标已完成后释放“当前申请占用”关系,但保留 COMPLETED 作为历史状态。
|
||||
// 否则退货箱虽显示 AVAILABLE,下一次贴标申请仍会被旧 relabel_application_id 拦截。
|
||||
carton.setRelabelApplicationId(null); carton.setRelabelNo(null);
|
||||
carton.setStatus("AVAILABLE"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
boolean awaitingTruck = requiresOutbound && "TRUCK".equals(application.getDeliveryMethod());
|
||||
if (!StringUtils.hasText(application.getOutboundOrderNo()) && !awaitingTruck) {
|
||||
carton.setRelabelApplicationId(null); carton.setRelabelNo(null);
|
||||
}
|
||||
carton.setStatus(StringUtils.hasText(application.getOutboundOrderNo()) ? "OUTBOUND_PENDING"
|
||||
: awaitingTruck ? "RELABEL_PENDING" : "AVAILABLE");
|
||||
carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
}
|
||||
insertHistory(customerId, application.getId(), before, application.getStatus(), operatorId,
|
||||
request.remark(), now);
|
||||
@@ -992,11 +1004,20 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return;
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
for (RelabelItem source : relabelItems) {
|
||||
if (source.getReturnCartonId() == null) continue;
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(source.getReturnCartonId());
|
||||
if (carton == null || !application.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !application.getId().equals(carton.getRelabelApplicationId())
|
||||
|| !"RELABEL_PENDING".equals(carton.getStatus())
|
||||
|| intValue(source.getNotifiedPackageCount()) != 1)
|
||||
throw conflict("退货整箱换标出库必须覆盖该箱全部 SKU 数量:" + source.getCartonMark());
|
||||
}
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(application.getCustomerId());
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RELABEL_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
|
||||
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); outbound.setOutboundType("RELABEL_OUTBOUND");
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> intValue(item.getNotifiedPackageCount())).sum());
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> item.getReturnCartonId() == null ? intValue(item.getNotifiedPackageCount()) : 1).sum());
|
||||
outbound.setPalletCount((int) relabelItems.stream().map(this::outboundSourceKey).distinct().count());
|
||||
outbound.setRecipientInfo(application.getDeliveryMethod()); outbound.setIdempotencyKey(key);
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
@@ -1012,9 +1033,13 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
item.setCustomerId(application.getCustomerId()); item.setOutboundOrderId(outbound.getId());
|
||||
item.setWarehousePalletId(position == null ? null : position.getPalletId());
|
||||
item.setPalletNo(position == null ? null : position.getPalletNo());
|
||||
item.setPackageCount(group.stream().mapToInt(value -> intValue(value.getNotifiedPackageCount())).sum());
|
||||
item.setPackageCount(group.stream().mapToInt(value -> value.getReturnCartonId() == null ? intValue(value.getNotifiedPackageCount()) : 1).sum());
|
||||
item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(group.stream().map(RelabelItem::getCartonMark).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
// Return-carton relabel picking must scan the original warehouse carton mark;
|
||||
// the requested/new carton mark is retained in the relabel snapshot only.
|
||||
item.setCartonMarks(group.stream()
|
||||
.map(value -> value.getReturnCartonId() == null ? value.getCartonMark() : value.getSourceCartonMark())
|
||||
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setDestinationWarehouseCode(group.stream().map(RelabelItem::getDestinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
item.setInboundCode(group.stream().map(RelabelItem::getSourceInboundCode).filter(StringUtils::hasText).distinct().collect(Collectors.joining(",")));
|
||||
@@ -1024,6 +1049,27 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(application.getCustomerId());
|
||||
task.setOutboundOrderId(outbound.getId()); task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId());
|
||||
task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); outboundTasks.insert(task);
|
||||
if (relabelItems.stream().allMatch(item -> item.getReturnCartonId() != null)) {
|
||||
Map<String, List<RelabelItem>> destinations = relabelItems.stream().collect(Collectors.groupingBy(
|
||||
item -> Objects.toString(item.getDestinationFbaWarehouseCode(), ""), LinkedHashMap::new, Collectors.toList()));
|
||||
int index = 0;
|
||||
for (var destination : destinations.entrySet()) {
|
||||
var delivery = new com.baiyue.lastleg.delivery.entity.DeliveryOrder();
|
||||
delivery.setId(IdWorker.getId()); delivery.setCustomerId(application.getCustomerId());
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", application.getOverseasWarehouseId(), delivery.getId()));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(application.getOverseasWarehouseId());
|
||||
delivery.setDeliveryGroupKey("RETURN_RELABEL:" + destination.getKey());
|
||||
delivery.setDestinationWarehouseCode(destination.getKey());
|
||||
delivery.setDestinationType(StringUtils.hasText(destination.getKey()) ? "FBA" : null);
|
||||
delivery.setContactName(application.getContactName()); delivery.setContactPhone(application.getContactPhone());
|
||||
delivery.setDeliveryMethod(application.getDeliveryMethod()); delivery.setStatus("WAIT_DELIVERY");
|
||||
delivery.setPackageCount(destination.getValue().size()); delivery.setPalletCount(0);
|
||||
delivery.setIdempotencyKey(key + ":DELIVERY:" + index++); delivery.setVersion(0L);
|
||||
delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
|
||||
deliveryOrders.insert(delivery);
|
||||
}
|
||||
}
|
||||
application.setOutboundOrderNo(outbound.getOutboundNo());
|
||||
}
|
||||
|
||||
@@ -1071,6 +1117,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
ReturnInventoryCarton carton = sourceInventory.returnCarton();
|
||||
item.setReturnCartonId(carton.getId()); item.setSourceCartonMark(carton.getCartonMark());
|
||||
item.setSourceInboundOrderNo(carton.getSourceReturnNo());
|
||||
item.setSourceInboundCode(carton.getSourceReturnNo());
|
||||
item.setSourceWarehouseId(carton.getOverseasWarehouseId());
|
||||
}
|
||||
item.setCreatedAt(now); items.insert(item);
|
||||
|
||||
+8
@@ -44,6 +44,14 @@ public class RelabelBindingGuard {
|
||||
} catch (Exception e) { throw conflict("组托记录无法读取,请核对后重试"); }
|
||||
}
|
||||
for (RelabelItem item : cartons) {
|
||||
if (item.getReturnCartonId() != null && !"RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
|
||||
var carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
if (carton == null || !app.getCustomerId().equals(carton.getCustomerId())) throw conflict("退货箱来源缺失");
|
||||
if (!"TRUCK".equals(app.getDeliveryMethod())) continue;
|
||||
if (!bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode()))
|
||||
throw conflict("退货箱尚未完成排车托盘绑定");
|
||||
continue;
|
||||
}
|
||||
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
|
||||
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
|
||||
if (carton == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode())
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.lastleg.returnreceipt.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/return-packing-plans")
|
||||
public class MerchantReturnPackingPlanController {
|
||||
@Resource private ReturnInventoryService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('merchant:overseas-warehouse:return-inventory:list')")
|
||||
public ApiResponse<PageResult<ReturnInventoryResponses.PackingPlan>> list(@AuthenticationPrincipal AuthenticatedUser user, @Valid ReturnInventoryRequests.PackingPlanQuery request) {
|
||||
return ApiResponse.success(service.customerPackingPlans(customer(user), request));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('merchant:overseas-warehouse:return-inventory:list')")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlan> create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ReturnInventoryRequests.PackingPlanCreate request) {
|
||||
return ApiResponse.success(service.createPackingPlan(user == null ? "merchant" : user.username(), customer(user), request));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null || "PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
}
|
||||
+38
@@ -35,9 +35,47 @@ public class PdaReturnInventoryController {
|
||||
return ApiResponse.success(service.merge(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/cartons/pack")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlanCompletion> pack(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReturnInventoryRequests.PackingExecution request) {
|
||||
return ApiResponse.success(service.pack(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}")
|
||||
public ApiResponse<ReturnInventoryResponses.CartonDetail> scanCarton(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.scanCarton(PdaWarehouseIdentity.require(user), cartonMark));
|
||||
}
|
||||
|
||||
@PostMapping("/cartons/merge-boxes")
|
||||
public ApiResponse<ReturnInventoryResponses.MergeResult> mergeBoxes(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReturnInventoryRequests.BoxMerge request) {
|
||||
return ApiResponse.success(service.mergeBoxes(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}/label")
|
||||
public ApiResponse<ReturnInventoryResponses.Label> label(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String cartonMark) {
|
||||
return ApiResponse.success(service.label(PdaWarehouseIdentity.require(user), cartonMark));
|
||||
}
|
||||
|
||||
@PostMapping("/cartons/{cartonMark}/putaway")
|
||||
public ApiResponse<ReturnInventoryResponses.PutawayResult> putaway(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String cartonMark, @Valid @RequestBody ReturnInventoryRequests.Putaway request) {
|
||||
return ApiResponse.success(service.putaway(PdaWarehouseIdentity.require(user), cartonMark, request));
|
||||
}
|
||||
|
||||
@GetMapping("/packing/plans")
|
||||
public ApiResponse<java.util.List<ReturnInventoryResponses.PackingPlan>> plans(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.listPackingPlans(PdaWarehouseIdentity.require(user)));
|
||||
}
|
||||
|
||||
@GetMapping("/packing/plans/{planNo}")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlan> plan(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo) {
|
||||
return ApiResponse.success(service.packingPlan(PdaWarehouseIdentity.require(user), planNo));
|
||||
}
|
||||
|
||||
@PostMapping("/packing/plans/{planNo}/complete")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlanCompletion> completePlan(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo, @Valid @RequestBody ReturnInventoryRequests.PackingPlanComplete request) {
|
||||
return ApiResponse.success(service.completePackingPlan(PdaWarehouseIdentity.require(user), planNo, request));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ public class PdaReturnReceiptController {
|
||||
String code = org.springframework.web.util.HtmlUtils.htmlEscape(pallet.palletCode());
|
||||
String returnNo = org.springframework.web.util.HtmlUtils.htmlEscape(request.returnNo());
|
||||
String sku = org.springframework.web.util.HtmlUtils.htmlEscape(request.skuCode());
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code + "</title><style>@page{size:100mm 70mm;margin:4mm}body{font-family:Arial,sans-serif;text-align:center;margin:0}h1{font-size:24px;margin:7mm 0 3mm}.code{font-size:17px;font-weight:700;word-break:break-all}.ref{font-size:11px;margin-top:4mm}</style></head><body><h1>退货托盘标签</h1><div class=\"code\">" + code + "</div><div class=\"ref\">退货单:" + returnNo + "<br>SKU:" + sku + "</div></body></html>";
|
||||
String html = "<!doctype html><html><head><meta charset=\"UTF-8\"><title>" + code + "</title><style>@page{size:100mm 70mm;margin:0}*{box-sizing:border-box}body{width:100mm;height:70mm;margin:0;padding:6mm 8mm;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;text-align:center;color:#111}.code{max-width:84mm;font-size:14px;font-weight:700;line-height:1.15;overflow-wrap:anywhere}.ref{max-width:84mm;margin-top:3mm;font-size:9px;line-height:1.35;overflow-wrap:anywhere}</style></head><body><div class=\"code\">" + code + "</div><div class=\"ref\">" + returnNo + "<br>" + sku + "</div></body></html>";
|
||||
return ApiResponse.success(new Pallet(pallet.palletCode(), pallet.palletId(), pallet.status(), html));
|
||||
}
|
||||
@PostMapping("/pallets/check") public ApiResponse<PdaReturnReceiptResponses.PalletCheck> checkPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+6
@@ -36,6 +36,12 @@ public class PlatformReturnInventoryController {
|
||||
return ApiResponse.success(service.platformMovementPage(request));
|
||||
}
|
||||
|
||||
@GetMapping("/packing-sources")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('platform:overseas-warehouse:return-cartons:list')")
|
||||
public ApiResponse<PageResult<ReturnInventoryResponses.PackingSource>> packingSources(@Valid ReturnInventoryRequests.PlatformQuery request) {
|
||||
return ApiResponse.success(service.platformPackingSourcePage(request));
|
||||
}
|
||||
|
||||
@GetMapping("/cartons/{cartonMark}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-cartons:list')")
|
||||
public ApiResponse<ReturnInventoryResponses.CartonDetail> carton(@PathVariable String cartonMark) {
|
||||
|
||||
+14
@@ -14,6 +14,20 @@ import org.springframework.web.bind.annotation.*;
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')")
|
||||
public class PlatformReturnOrderController {
|
||||
@Resource private ReturnReceiptService service;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService inventory;
|
||||
@PostMapping("/{returnNo}/receipts/{receiptNo}/repair-cartons")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
|
||||
public ApiResponse<java.util.List<ReturnOrderResponses.CartonLabel>> repairCartons(
|
||||
@org.springframework.security.core.annotation.AuthenticationPrincipal com.baiyue.common.security.AuthenticatedUser user,
|
||||
@RequestHeader("X-Customer-Id") String customerId, @PathVariable String returnNo, @PathVariable String receiptNo,
|
||||
@Valid @RequestBody com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.ReceiptCartonRepair request) {
|
||||
return ApiResponse.success(inventory.repairReceiptCartons("platform:" + user.username(), customerId, returnNo, receiptNo, request));
|
||||
}
|
||||
@GetMapping("/{returnNo}")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> detail(@RequestHeader("X-Customer-Id") String customerId,
|
||||
@PathVariable String returnNo) {
|
||||
return ApiResponse.success(service.detail(customerId, returnNo));
|
||||
}
|
||||
@PostMapping("/{returnNo}/review")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> review(@RequestHeader("X-Customer-Id") String customerId,
|
||||
@PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Review request) {
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.lastleg.returnreceipt.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/return-packing-plans")
|
||||
public class PlatformReturnPackingPlanController {
|
||||
@Resource private ReturnInventoryService service;
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlan> create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ReturnInventoryRequests.PackingPlanCreate request) {
|
||||
return ApiResponse.success(service.createPackingPlan(user == null ? "platform" : user.username(), request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list')")
|
||||
public ApiResponse<com.baiyue.common.core.api.PageResult<ReturnInventoryResponses.PackingPlan>> list(@Valid ReturnInventoryRequests.PackingPlanQuery request) {
|
||||
return ApiResponse.success(service.platformPackingPlans(request));
|
||||
}
|
||||
|
||||
@PostMapping("/{planNo}/review")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlan> review(@PathVariable String planNo) {
|
||||
return ApiResponse.success(service.reviewPackingPlanPlatform(planNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{planNo}/cancel")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:return-inventory:list') and hasAuthority('lastleg:inventory:manage')")
|
||||
public ApiResponse<ReturnInventoryResponses.PackingPlan> cancel(@PathVariable String planNo) {
|
||||
return ApiResponse.success(service.cancelPackingPlan(planNo));
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class PdaReturnReceiptRequests {
|
||||
private PdaReturnReceiptRequests() { }
|
||||
@@ -18,10 +19,25 @@ public final class PdaReturnReceiptRequests {
|
||||
public record Confirm(@NotBlank @Size(max = 128) String returnNo,
|
||||
@Size(max = 128) String trackingNo,
|
||||
@NotBlank @Size(max = 16) String startedBy,
|
||||
@NotBlank @Size(max = 128) String palletCode,
|
||||
@Size(max = 128) String palletCode,
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 16) String unitType,
|
||||
@Size(max = 128) String unitCode,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
Long totalBoxCount) {
|
||||
public Confirm(String returnNo, String trackingNo, String startedBy, String palletCode,
|
||||
List<@Valid Item> items, String idempotencyKey) {
|
||||
this(returnNo, trackingNo, startedBy, palletCode, items, idempotencyKey,
|
||||
null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
public record Item(@NotBlank @Size(max = 128) String skuCode,
|
||||
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = ReturnQuantityDeserializer.class)
|
||||
@Positive long receivedQuantity) { }
|
||||
@Positive long receivedQuantity,
|
||||
Long boxCount) {
|
||||
public Item(String skuCode, long receivedQuantity) {
|
||||
this(skuCode, receivedQuantity, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -16,5 +16,7 @@ public final class PdaReturnReceiptResponses {
|
||||
public record Item(String skuCode, String productNameZh, long expectedQuantity, long receivedQuantity) { }
|
||||
public record Confirmed(String returnNo, String receiptNo, String receiptBatchNo, String palletCode, String orderStatus,
|
||||
boolean fullyMatched, long expectedQuantity, long receivedQuantity,
|
||||
LocalDateTime signedAt) { }
|
||||
LocalDateTime signedAt, String unitType, String unitCode,
|
||||
String cartonMark, String printableHtml, long totalBoxCount,
|
||||
List<ReturnOrderResponses.CartonLabel> labels) { }
|
||||
}
|
||||
|
||||
+57
-1
@@ -14,6 +14,8 @@ import java.util.List;
|
||||
|
||||
public final class ReturnInventoryRequests {
|
||||
private ReturnInventoryRequests() { }
|
||||
public record ReceiptCartonRepair(@Positive long confirmedBoxCount,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String overseasWarehouseId,
|
||||
String skuCode, String cartonMark, String status, String returnNo) {
|
||||
@@ -31,7 +33,46 @@ public final class ReturnInventoryRequests {
|
||||
public PlatformQuery { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record PackingQuery(@Size(max = 128) String returnNo) { }
|
||||
public record PackingQuery(@Size(max = 128) String returnNo, @Size(max = 128) String skuCode) { }
|
||||
|
||||
public record PackingPlanQuery(@Min(1) long page, @Min(1) @Max(200) long size, String status, @Size(max = 128) String planNo,
|
||||
@Size(max = 64) String customerId, @Size(max = 64) String overseasWarehouseId, @Size(max = 128) String skuCode) {
|
||||
public PackingPlanQuery { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record PackingPlanCreate(@NotEmpty @Size(max = 100) List<@NotBlank @Size(max = 128) String> returnNos,
|
||||
@NotBlank @Size(max = 40) String mode,
|
||||
@NotEmpty @Size(max = 100) List<@Valid PlanBox> boxes,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 500) List<@NotBlank String> sourceCartonMarks) {
|
||||
public PackingPlanCreate(List<String> returnNos, String mode, List<PlanBox> boxes, String remark, String idempotencyKey) {
|
||||
this(returnNos, mode, boxes, remark, idempotencyKey, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public record PlanBox(@Size(max = 100) List<@Valid PlanItem> items,
|
||||
@Positive BigDecimal length, @Positive BigDecimal width,
|
||||
@Positive BigDecimal height, @Positive BigDecimal weight,
|
||||
@Size(max = 128) String targetCartonMark) {
|
||||
public PlanBox(List<PlanItem> items, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight) {
|
||||
this(items, length, width, height, weight, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record PlanItem(@NotBlank @Size(max = 128) String skuCode,
|
||||
@Positive long quantity) { }
|
||||
|
||||
public record PackingPlanComplete(@NotEmpty @Size(max = 100) List<@Valid PlanBox> boxes,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) List<@Valid SourceDeduction> sourceDeductions) {
|
||||
public PackingPlanComplete(List<PlanBox> boxes, String idempotencyKey) { this(boxes, idempotencyKey, null); }
|
||||
}
|
||||
public record SourceDeduction(@NotBlank String cartonMark, @NotBlank String skuCode, @Positive long quantity) { }
|
||||
|
||||
public record PackingExecution(@NotEmpty @Size(max = 100) List<@NotBlank String> returnNos,
|
||||
@NotEmpty @Size(max = 100) List<@Valid PlanBox> boxes,
|
||||
@NotBlank @Size(max = 180) String idempotencyKey) { }
|
||||
|
||||
public record Merge(@NotEmpty @Size(min = 1, max = 100) List<@NotBlank @Size(max = 128) String> returnNos,
|
||||
@NotEmpty @Size(max = 500) List<@Valid ValidItem> items,
|
||||
@@ -44,4 +85,19 @@ public final class ReturnInventoryRequests {
|
||||
public record ValidItem(@NotBlank @Size(max = 128) String skuCode,
|
||||
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = ReturnQuantityDeserializer.class)
|
||||
@Positive long quantity) { }
|
||||
|
||||
public record Putaway(@NotBlank @Size(max = 128) String locationCode,
|
||||
@Size(max = 128) String palletCode,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) { }
|
||||
|
||||
public record BoxMerge(@NotEmpty @Size(max = 100) List<@NotBlank String> sourceCartonMarks,
|
||||
String targetCartonMark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) List<@Valid SourceDeduction> sourceDeductions,
|
||||
@Positive BigDecimal length, @Positive BigDecimal width,
|
||||
@Positive BigDecimal height, @Positive BigDecimal weight) {
|
||||
public BoxMerge(List<String> sourceCartonMarks, String targetCartonMark, String idempotencyKey) {
|
||||
this(sourceCartonMarks, targetCartonMark, idempotencyKey, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-7
@@ -6,6 +6,8 @@ import java.util.List;
|
||||
|
||||
public final class ReturnInventoryResponses {
|
||||
private ReturnInventoryResponses() { }
|
||||
public record PackingSource(String cartonMark, String sourceReturnNo, String sourceUnitType,
|
||||
List<CartonItem> items) { }
|
||||
|
||||
public record CartonSummary(String cartonMark, String overseasWarehouseId, String sourceBatchNo,
|
||||
String sourceReturnNo, String status, int skuTypes, long totalQuantity,
|
||||
@@ -20,22 +22,42 @@ public final class ReturnInventoryResponses {
|
||||
public record CartonDetail(String cartonMark, String customerId, String overseasWarehouseId, String sourceBatchNo, String sourceReturnNo, String status,
|
||||
String fbaCartonMark, String destinationWarehouseCode, String destinationWarehouseName, String relabelNo, String relabelStatus,
|
||||
int skuTypes, long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height,
|
||||
BigDecimal weight, LocalDateTime receivedAt, List<CartonItem> items) { }
|
||||
public record CartonItem(String skuCode, String productNameZh, long quantity) { }
|
||||
BigDecimal weight, LocalDateTime receivedAt, List<CartonItem> items,
|
||||
String locationCode, String palletCode, LocalDateTime putawayAt, List<MergeHistory> mergeHistory) { }
|
||||
public record MergeHistory(String planNo, String sourceCartonMark, String targetCartonMark, String returnNo,
|
||||
String skuCode, long quantity, LocalDateTime createdAt) { }
|
||||
public record CartonItem(String skuCode, String productNameZh, long quantity, long availableQuantity) {
|
||||
public CartonItem(String skuCode, String productNameZh, long quantity) { this(skuCode, productNameZh, quantity, quantity); }
|
||||
}
|
||||
public record Label(String cartonMark, String overseasWarehouseId, String customerId, String sourceBatchNo, String sourceReturnNo,
|
||||
long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
List<CartonItem> items, LocalDateTime generatedAt, String printableHtml) { }
|
||||
public record MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label) { }
|
||||
public record MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label, String planNo) {
|
||||
public MergeResult(String cartonMark, String skuCode, long totalQuantity, Label label) { this(cartonMark, skuCode, totalQuantity, label, null); }
|
||||
}
|
||||
public record PutawayResult(String cartonMark, String locationCode, String palletCode, LocalDateTime putawayAt) { }
|
||||
public record PackingOrder(String returnNo, String customerId, String customerReferenceNo, String status) { }
|
||||
public record PackingSku(String skuCode, String productNameZh, long availableQuantity) { }
|
||||
public record PackingPreview(List<PackingOrder> orders, String customerId, List<PackingSku> skus) { }
|
||||
public record PackingCustomer(String customerId, List<PackingOrder> orders, List<PackingSku> skus) { }
|
||||
public record PackingPreview(List<PackingOrder> orders, String customerId, List<PackingSku> skus, List<PackingCustomer> customers) {
|
||||
public PackingPreview(List<PackingOrder> orders, String customerId, List<PackingSku> skus) { this(orders, customerId, skus, List.of()); }
|
||||
}
|
||||
public record PackingPlan(String planNo, String customerId, String overseasWarehouseId, List<String> returnNos,
|
||||
String mode, String status, List<PlanBox> boxes, String remark, LocalDateTime pushedAt,
|
||||
List<String> sourceCartonMarks, List<PlanSource> sources,
|
||||
LocalDateTime createdAt, List<CartonDetail> resultCartons, LocalDateTime completedAt) { }
|
||||
public record PlanSource(String cartonMark, List<PlanItem> items) { }
|
||||
public record PlanBox(int boxNo, List<PlanItem> items, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight) { }
|
||||
public record PlanItem(String skuCode, long quantity) { }
|
||||
public record PackingPlanCompletion(String planNo, List<Label> labels) { }
|
||||
public record PlatformStock(String customerId, String overseasWarehouseId, String skuCode, String productNameZh,
|
||||
long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { }
|
||||
long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt,
|
||||
long looseAvailableQuantity) { }
|
||||
public record PlatformCarton(String customerId, String overseasWarehouseId, String cartonMark, String sourceReturnNo,
|
||||
String sourceBatchNo, String status, int skuTypes, long totalQuantity,
|
||||
String sourceUnitType, String sourceBatchNo, String status, int skuTypes, long totalQuantity,
|
||||
String fbaCartonMark, String destinationWarehouseCode, String destinationWarehouseName, String relabelNo, String relabelStatus,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
LocalDateTime createdAt) { }
|
||||
LocalDateTime createdAt, String locationCode, String palletCode, LocalDateTime putawayAt) { }
|
||||
public record PlatformMovement(String customerId, String overseasWarehouseId, String skuCode, String productNameZh,
|
||||
String movementType, String referenceNo, long totalDelta, long availableDelta,
|
||||
LocalDateTime occurredAt) { }
|
||||
|
||||
+35
-4
@@ -12,12 +12,43 @@ public final class ReturnOrderResponses {
|
||||
List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications) { }
|
||||
public record Item(String skuCode, String productNameZh, long expectedQuantity,
|
||||
long receivedQuantity) { }
|
||||
public record Detail(Summary order, String remark, List<Item> items, List<Receipt> receipts, List<PalletDetail> pallets, List<Photo> photos) { }
|
||||
public record Detail(Summary order, String remark, List<Item> items, List<Receipt> receipts, List<PalletDetail> pallets, List<Photo> photos,
|
||||
List<PackingDetail> packingDetails) {
|
||||
public Detail(Summary order, String remark, List<Item> items, List<Receipt> receipts, List<PalletDetail> pallets, List<Photo> photos) {
|
||||
this(order, remark, items, receipts, pallets, photos, List.of());
|
||||
}
|
||||
}
|
||||
public record PackingDetail(String sourceCartonMark, String targetCartonMark, String returnNo,
|
||||
String skuCode, long quantity, LocalDateTime createdAt) { }
|
||||
public record Photo(long fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
public record PalletDetail(String palletCode, int skuTypes, long totalQuantity, List<PalletItem> items) { }
|
||||
public record PalletItem(String skuCode, String productNameZh, long quantity) { }
|
||||
public record PalletDetail(String palletCode, int skuTypes, long totalQuantity, List<PalletItem> items,
|
||||
long totalBoxCount, String unitType, String unitCode) {
|
||||
public PalletDetail(String palletCode, int skuTypes, long totalQuantity, List<PalletItem> items) {
|
||||
this(palletCode, skuTypes, totalQuantity, items, 0, "PALLET", palletCode);
|
||||
}
|
||||
}
|
||||
public record PalletItem(String skuCode, String productNameZh, long quantity, long boxCount) {
|
||||
public PalletItem(String skuCode, String productNameZh, long quantity) { this(skuCode, productNameZh, quantity, 0); }
|
||||
}
|
||||
public record Receipt(String receiptNo, String receiptBatchNo, String palletCode, String trackingNo, String startedBy,
|
||||
long receivedQuantity, String operatorName, LocalDateTime signedAt) { }
|
||||
long receivedQuantity, String operatorName, LocalDateTime signedAt,
|
||||
String unitType, String unitCode, long boxCount, List<ReceiptItem> items,
|
||||
String cartonMark, String printableHtml, List<CartonLabel> labels, boolean labelRepairRequired) {
|
||||
public Receipt(String receiptNo, String receiptBatchNo, String palletCode, String trackingNo, String startedBy,
|
||||
long receivedQuantity, String operatorName, LocalDateTime signedAt,
|
||||
String unitType, String unitCode, long boxCount, List<ReceiptItem> items,
|
||||
String cartonMark, String printableHtml, List<CartonLabel> labels) {
|
||||
this(receiptNo, receiptBatchNo, palletCode, trackingNo, startedBy, receivedQuantity, operatorName, signedAt,
|
||||
unitType, unitCode, boxCount, items, cartonMark, printableHtml, labels, false);
|
||||
}
|
||||
public Receipt(String receiptNo, String receiptBatchNo, String palletCode, String trackingNo, String startedBy,
|
||||
long receivedQuantity, String operatorName, LocalDateTime signedAt) {
|
||||
this(receiptNo, receiptBatchNo, palletCode, trackingNo, startedBy, receivedQuantity, operatorName, signedAt,
|
||||
"PALLET", palletCode, 0, List.of(), null, null, List.of());
|
||||
}
|
||||
}
|
||||
public record CartonLabel(String cartonMark, String printableHtml) { }
|
||||
public record ReceiptItem(String skuCode, String productNameZh, long quantity, long boxCount) { }
|
||||
public record RelabelCandidate(Long cartonId, String cartonMark, String sourceReturnNo, String status,
|
||||
long totalQuantity, List<Item> items, String relabelStatus) { }
|
||||
public record RelabelCreated(String relabelNo, Long relabelApplicationId, String returnNo,
|
||||
|
||||
+4
@@ -18,6 +18,8 @@ public class ReturnInventoryCarton {
|
||||
private Long sourceReceiptId;
|
||||
private String sourceBatchNo;
|
||||
private String sourceReturnNo;
|
||||
private String sourceUnitType;
|
||||
private String sourceUnitCode;
|
||||
private String palletCode;
|
||||
private String cartonMark;
|
||||
private String fbaCartonMark;
|
||||
@@ -38,4 +40,6 @@ public class ReturnInventoryCarton {
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime mergedAt;
|
||||
private String newCartonMark;
|
||||
private String locationCode;
|
||||
private LocalDateTime putawayAt;
|
||||
}
|
||||
|
||||
+1
@@ -14,4 +14,5 @@ public class ReturnInventoryCartonItem {
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
private Long packingReservedQuantity;
|
||||
}
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ public class ReturnInventoryOperation {
|
||||
private String requestHash;
|
||||
private String operationType;
|
||||
private Long resultCartonId;
|
||||
private String resultJson;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.returnreceipt.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_return_packing_plan")
|
||||
public class ReturnPackingPlan {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String planNo;
|
||||
private String customerId;
|
||||
private String overseasWarehouseId;
|
||||
private String returnNosJson;
|
||||
private String boxesJson;
|
||||
private String sourceItemsJson;
|
||||
private String resultMarksJson;
|
||||
private String resultLabelsJson;
|
||||
private String requestHash;
|
||||
private String completionHash;
|
||||
private String mode;
|
||||
private String status;
|
||||
private String remark;
|
||||
private String idempotencyKey;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime pushedAt;
|
||||
private LocalDateTime completedAt;
|
||||
}
|
||||
+8
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@TableName("ll_return_receipt")
|
||||
@@ -15,6 +16,13 @@ public class ReturnReceipt {
|
||||
private String customerId;
|
||||
private String receiptNo;
|
||||
private String receiptBatchNo;
|
||||
private String unitType;
|
||||
private String unitCode;
|
||||
private Long totalBoxCount;
|
||||
private BigDecimal length;
|
||||
private BigDecimal width;
|
||||
private BigDecimal height;
|
||||
private BigDecimal weight;
|
||||
private String palletCode;
|
||||
private String cartonMark;
|
||||
private String trackingNo;
|
||||
|
||||
+1
@@ -14,4 +14,5 @@ public class ReturnReceiptItem {
|
||||
private String customerId;
|
||||
private String skuCode;
|
||||
private Long receivedQuantity;
|
||||
private Long boxCount;
|
||||
}
|
||||
|
||||
+23
-2
@@ -12,6 +12,11 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
|
||||
String FREE_STOCK_COLUMNS = "b.id,b.customer_id,b.overseas_warehouse_id,b.sku_code,b.product_name_zh,b.total_quantity,b.carton_quantity,b.created_at,b.updated_at," +
|
||||
"LEAST(b.available_quantity,COALESCE((SELECT SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))) " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " +
|
||||
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id AND i.sku_code=b.sku_code " +
|
||||
"AND c.status='AVAILABLE'),0)) AS available_quantity";
|
||||
@Select("SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} FOR UPDATE")
|
||||
ReturnInventoryBalance lockSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@@ -39,13 +44,29 @@ public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventory
|
||||
int adjustCartonQuantity(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("cartons") long cartons);
|
||||
|
||||
@Update("UPDATE ll_return_inventory_balance SET available_quantity=available_quantity-#{quantity},updated_at=NOW(3) " +
|
||||
"WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} AND available_quantity>=#{quantity}")
|
||||
int reserveAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("quantity") long quantity);
|
||||
|
||||
@Select("SELECT COALESCE(available_quantity,0) FROM ll_return_inventory_balance WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} LIMIT 1")
|
||||
long availableQuantity(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Update("UPDATE ll_return_inventory_balance SET total_quantity=total_quantity-#{quantity}," +
|
||||
"available_quantity=GREATEST(0,available_quantity-#{quantity}),carton_quantity=carton_quantity-1,updated_at=NOW(3) " +
|
||||
"WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} " +
|
||||
"AND total_quantity>=#{quantity} AND carton_quantity>=1")
|
||||
int shipCartonSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("quantity") long quantity);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if></script>")
|
||||
long countPage(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
@Select("<script>SELECT " + FREE_STOCK_COLUMNS + " FROM ll_return_inventory_balance b WHERE customer_id=#{customerId} " +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if>" +
|
||||
"ORDER BY updated_at DESC,id DESC LIMIT #{offset},#{size}</script>")
|
||||
@@ -58,7 +79,7 @@ public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventory
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if></script>")
|
||||
long platformCount(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_return_inventory_balance WHERE 1=1 " +
|
||||
@Select("<script>SELECT " + FREE_STOCK_COLUMNS + " FROM ll_return_inventory_balance b WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND customer_id LIKE CONCAT('%',#{customerId},'%') </if>" +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"<if test='skuCode != null and skuCode != \"\"'>AND sku_code LIKE CONCAT('%',#{skuCode},'%') </if>" +
|
||||
|
||||
+10
@@ -11,8 +11,18 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryCartonItemMapper extends BaseMapper<ReturnInventoryCartonItem> {
|
||||
@Select("SELECT COALESCE(SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))),0) " +
|
||||
"FROM ll_return_inventory_carton_item i JOIN ll_return_inventory_carton c ON c.id=i.carton_id " +
|
||||
"WHERE c.customer_id=#{customerId} AND c.overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND c.source_unit_type='PARCEL' AND c.status='AVAILABLE' AND i.sku_code=#{skuCode}")
|
||||
long looseAvailableQuantity(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode);
|
||||
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} ORDER BY sku_code")
|
||||
List<ReturnInventoryCartonItem> listCarton(@Param("cartonId") long cartonId);
|
||||
@Select("<script>SELECT * FROM ll_return_inventory_carton_item WHERE carton_id IN " +
|
||||
"<foreach collection='cartonIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"ORDER BY carton_id,sku_code</script>")
|
||||
List<ReturnInventoryCartonItem> listCartons(@Param("cartonIds") List<Long> cartonIds);
|
||||
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} ORDER BY sku_code FOR UPDATE")
|
||||
List<ReturnInventoryCartonItem> lockCarton(@Param("cartonId") long cartonId);
|
||||
|
||||
|
||||
+14
@@ -10,6 +10,12 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND status='AVAILABLE' ORDER BY carton_mark")
|
||||
List<ReturnInventoryCarton> packingAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
|
||||
|
||||
@Update("UPDATE ll_return_inventory_carton SET location_code=NULL,putaway_at=NULL WHERE id=#{id}")
|
||||
int clearPutaway(@Param("id") long id);
|
||||
|
||||
String MERCHANT_FILTER = " FROM ll_return_inventory_carton c WHERE c.customer_id=#{customerId} " +
|
||||
"<if test='query.overseasWarehouseId != null and query.overseasWarehouseId != \"\"'>AND c.overseas_warehouse_id=#{query.overseasWarehouseId} </if>" +
|
||||
"<if test='query.cartonMark != null and query.cartonMark != \"\"'>AND c.carton_mark LIKE CONCAT('%',#{query.cartonMark},'%') </if>" +
|
||||
@@ -72,6 +78,9 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} LIMIT 1")
|
||||
ReturnInventoryCarton findReceipt(@Param("receiptId") long receiptId);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} ORDER BY id")
|
||||
List<ReturnInventoryCarton> listReceipt(@Param("receiptId") long receiptId);
|
||||
|
||||
@Select("SELECT c.* FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.customer_id=#{customerId} AND c.overseas_warehouse_id=#{warehouseId} AND c.pallet_code=#{palletCode} AND c.status='AVAILABLE' ORDER BY c.id LIMIT 1 FOR UPDATE")
|
||||
ReturnInventoryCarton findPalletSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
|
||||
@@ -81,6 +90,11 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE overseas_warehouse_id=#{warehouseId} AND pallet_code=#{palletCode} AND status='AVAILABLE' ORDER BY id")
|
||||
List<ReturnInventoryCarton> listPalletInventory(@Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND pallet_code=#{palletCode} AND status IN ('AVAILABLE','RELABEL_PENDING','OUTBOUND_PENDING') ORDER BY id")
|
||||
List<ReturnInventoryCarton> listActivePallet(@Param("warehouseId") String warehouseId,
|
||||
@Param("palletCode") String palletCode);
|
||||
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND carton_mark=#{cartonMark} LIMIT 1")
|
||||
ReturnInventoryCarton findCustomerMark(@Param("customerId") String customerId, @Param("cartonMark") String cartonMark);
|
||||
|
||||
|
||||
+2
@@ -19,4 +19,6 @@ public interface ReturnOrderItemMapper extends BaseMapper<ReturnOrderItem> {
|
||||
"WHERE o.overseas_warehouse_id=#{warehouseId} AND o.status IN ('PENDING_RECEIPT','PARTIALLY_RECEIVED') " +
|
||||
"AND i.sku_code=#{skuCode} AND i.received_quantity<i.expected_quantity ORDER BY o.created_at,o.id LIMIT 50")
|
||||
List<ReturnOrderItem> activeSkuCandidates(@Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode);
|
||||
@Select("SELECT i.* FROM ll_return_order_item i JOIN ll_return_order o ON o.id=i.return_order_id WHERE o.overseas_warehouse_id=#{warehouseId} AND o.status='COMPLETED' AND i.sku_code=#{skuCode} AND i.received_quantity>0 ORDER BY o.created_at,o.id LIMIT 100")
|
||||
List<ReturnOrderItem> completedSkuCandidates(@Param("warehouseId") String warehouseId, @Param("skuCode") String skuCode);
|
||||
}
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnOrderMapper extends BaseMapper<ReturnOrder> {
|
||||
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND status='COMPLETED' ORDER BY created_at,id")
|
||||
List<ReturnOrder> completedCustomer(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
|
||||
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
ReturnOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_return_order WHERE customer_id=#{customerId} AND return_no=#{returnNo} LIMIT 1")
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnPackingPlan;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnPackingPlanMapper extends BaseMapper<ReturnPackingPlan> {
|
||||
@Select("SELECT * FROM ll_return_packing_plan WHERE plan_no=#{planNo} LIMIT 1")
|
||||
ReturnPackingPlan findNo(@Param("planNo") String planNo);
|
||||
@Select("SELECT * FROM ll_return_packing_plan WHERE plan_no=#{planNo} LIMIT 1 FOR UPDATE")
|
||||
ReturnPackingPlan lockNo(@Param("planNo") String planNo);
|
||||
@Select("SELECT * FROM ll_return_packing_plan WHERE overseas_warehouse_id=#{warehouseId} AND status IN ('READY','IN_PROGRESS') ORDER BY created_at ASC,id ASC LIMIT 100")
|
||||
List<ReturnPackingPlan> listPending(@Param("warehouseId") String warehouseId);
|
||||
@Select("SELECT COUNT(1) FROM ll_return_packing_plan WHERE customer_id=#{customerId} AND (#{status} IS NULL OR status=#{status})")
|
||||
long countCustomer(@Param("customerId") String customerId, @Param("status") String status);
|
||||
@Select("SELECT * FROM ll_return_packing_plan WHERE customer_id=#{customerId} AND (#{status} IS NULL OR status=#{status}) ORDER BY created_at DESC,id DESC LIMIT #{offset},#{size}")
|
||||
List<ReturnPackingPlan> listCustomer(@Param("customerId") String customerId, @Param("status") String status, @Param("offset") long offset, @Param("size") long size);
|
||||
String PLATFORM_FILTER = " WHERE (#{status} IS NULL OR status=#{status}) AND (#{planNo} IS NULL OR LOCATE(#{planNo},plan_no)>0) AND (#{customerId} IS NULL OR customer_id=#{customerId}) AND (#{warehouseId} IS NULL OR overseas_warehouse_id=#{warehouseId}) AND (#{sku} IS NULL OR LOCATE(#{sku},JSON_UNQUOTE(JSON_EXTRACT(boxes_json,'$[*].items[*].skuCode')))>0)";
|
||||
@Select("SELECT COUNT(1) FROM ll_return_packing_plan" + PLATFORM_FILTER)
|
||||
long countPlatform(@Param("status") String status, @Param("planNo") String planNo, @Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("sku") String sku);
|
||||
@Select("SELECT * FROM ll_return_packing_plan" + PLATFORM_FILTER + " ORDER BY created_at DESC,id DESC LIMIT #{offset},#{size}")
|
||||
List<ReturnPackingPlan> listPlatform(@Param("status") String status, @Param("planNo") String planNo, @Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("sku") String sku, @Param("offset") long offset, @Param("size") long size);
|
||||
@Select("SELECT * FROM ll_return_packing_plan WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1 FOR UPDATE")
|
||||
ReturnPackingPlan lockCustomerKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Update("UPDATE ll_return_packing_plan SET status=#{status},updated_at=#{updatedAt},pushed_at=#{pushedAt},completed_at=#{completedAt} WHERE id=#{id}")
|
||||
int updateStatus(@Param("id") long id, @Param("status") String status, @Param("updatedAt") java.time.LocalDateTime updatedAt, @Param("pushedAt") java.time.LocalDateTime pushedAt, @Param("completedAt") java.time.LocalDateTime completedAt);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** Immutable movements retain original return ownership when cartons are merged again. */
|
||||
@Mapper
|
||||
public interface ReturnPackingTraceMapper {
|
||||
record Trace(long id, String sourceCartonMark, String targetCartonMark, String returnNo,
|
||||
String skuCode, long quantity, LocalDateTime createdAt) { }
|
||||
record Ownership(String returnNo, long quantity) { }
|
||||
|
||||
@Insert("INSERT INTO ll_return_packing_trace(id,customer_id,operation_id,source_carton_mark,target_carton_mark,return_no,sku_code,quantity,created_at) VALUES(#{id},#{customerId},#{operationId},#{source},#{target},#{returnNo},#{sku},#{quantity},#{at})")
|
||||
int insert(@Param("id") long id, @Param("customerId") String customerId, @Param("operationId") long operationId,
|
||||
@Param("source") String source, @Param("target") String target, @Param("returnNo") String returnNo,
|
||||
@Param("sku") String sku, @Param("quantity") long quantity, @Param("at") LocalDateTime at);
|
||||
|
||||
@Select("SELECT return_no, SUM(CASE WHEN target_carton_mark=#{mark} THEN quantity ELSE -quantity END) AS quantity FROM ll_return_packing_trace WHERE customer_id=#{customerId} AND sku_code=#{sku} AND (source_carton_mark=#{mark} OR target_carton_mark=#{mark}) GROUP BY return_no HAVING quantity>0 ORDER BY return_no")
|
||||
List<Ownership> ownership(@Param("customerId") String customerId, @Param("mark") String mark, @Param("sku") String sku);
|
||||
|
||||
@Select("SELECT id,source_carton_mark,target_carton_mark,return_no,sku_code,quantity,created_at FROM ll_return_packing_trace WHERE customer_id=#{customerId} AND return_no=#{returnNo} ORDER BY created_at,id")
|
||||
List<Trace> listReturn(@Param("customerId") String customerId, @Param("returnNo") String returnNo);
|
||||
|
||||
@Select("SELECT JSON_UNQUOTE(JSON_EXTRACT(o.result_json,'$.planNo')) AS plan_no,t.source_carton_mark,t.target_carton_mark,t.return_no,t.sku_code,t.quantity,t.created_at FROM ll_return_packing_trace t JOIN ll_return_inventory_operation o ON o.id=t.operation_id WHERE t.customer_id=#{customerId} AND o.operation_type='MERGE' AND t.source_carton_mark<>'' AND (t.source_carton_mark=#{mark} OR t.target_carton_mark=#{mark}) ORDER BY t.created_at DESC,t.id DESC")
|
||||
List<com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.MergeHistory> listCartonHistory(@Param("customerId") String customerId, @Param("mark") String mark);
|
||||
}
|
||||
+2
-2
@@ -3,12 +3,12 @@ package com.baiyue.lastleg.returnreceipt.service;
|
||||
import com.google.zxing.oned.Code128Writer;
|
||||
|
||||
/** Inline vector bars remain printable even when browser background printing is disabled. */
|
||||
final class ReturnCartonBarcode {
|
||||
public final class ReturnCartonBarcode {
|
||||
private static final int QUIET_ZONE = 10;
|
||||
|
||||
private ReturnCartonBarcode() { }
|
||||
|
||||
static String svg(String cartonMark) {
|
||||
public static String svg(String cartonMark) {
|
||||
boolean[] modules = new Code128Writer().encode(cartonMark);
|
||||
int width = modules.length + QUIET_ZONE * 2;
|
||||
StringBuilder svg = new StringBuilder("<svg xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"箱号条码\" viewBox=\"0 0 ")
|
||||
|
||||
+710
-91
@@ -5,6 +5,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnOrderResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOperation;
|
||||
@@ -18,6 +19,10 @@ import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryQueryMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryMovementMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.model.ReturnInventoryRow;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnPackingPlan;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingPlanMapper;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
@@ -40,8 +45,11 @@ import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
||||
/** Return stock is deliberately separate from normal inbound stock because it has no putaway location yet. */
|
||||
/** Return stock keeps its own receipt ownership, carton inventory and putaway facts. */
|
||||
@Service
|
||||
public class ReturnInventoryService {
|
||||
public static final String AVAILABLE = "AVAILABLE";
|
||||
@@ -55,10 +63,319 @@ public class ReturnInventoryService {
|
||||
@Resource private ReturnInventoryMovementMapper movements;
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper orders;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderItemMapper orderItems;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptMapper receiptMapper;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnReceiptItemMapper receiptItemMapper;
|
||||
@Resource private LastlegOrderMapper lastlegOrders;
|
||||
@Resource private LastlegOrderDetailMapper lastlegOrderDetails;
|
||||
@Resource private com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway physicalPallets;
|
||||
@Resource private ReturnPackingPlanMapper packingPlans;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper packingTrace;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway warehouseResources;
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PutawayResult putaway(PdaWarehouseIdentity identity, String cartonMark, ReturnInventoryRequests.Putaway request) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(required(cartonMark, "箱唛不能为空"));
|
||||
if (carton == null || !String.valueOf(identity.overseasWarehouseId()).equals(carton.getOverseasWarehouseId())) throw notFound("当前仓没有该退货箱");
|
||||
String digest = hash(List.of(cartonMark, request));
|
||||
ReturnInventoryOperation replay = operations.lockKey(identity.operatorKey(), request.idempotencyKey());
|
||||
if (replay != null) {
|
||||
if (!"PUTAWAY".equals(replay.getOperationType()) || !digest.equals(replay.getRequestHash())) throw conflict("幂等键已用于不同的上架请求");
|
||||
return read(replay.getResultJson(), new TypeReference<ReturnInventoryResponses.PutawayResult>() { });
|
||||
}
|
||||
if (!AVAILABLE.equals(carton.getStatus()) || cartonItems.lockCarton(carton.getId()).stream().anyMatch(i -> value(i.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("退货箱已被占用或不可上架");
|
||||
var location = warehouseResources.resolveLocation(identity.overseasWarehouseId(), required(request.locationCode(), "库位码不能为空"));
|
||||
if (location == null || !List.of("ACTIVE", "IDLE", "OCCUPIED").contains(location.status())
|
||||
|| "VIRTUAL".equalsIgnoreCase(location.locationType())) throw conflict("目标库位不存在、已停用或为虚拟收货位");
|
||||
String palletCode = text(request.palletCode());
|
||||
if (palletCode != null) {
|
||||
var pallet = physicalPallets.resolve(palletCode);
|
||||
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId()
|
||||
|| !List.of("IDLE", "OCCUPIED").contains(pallet.status()) || pallet.locationId() == null || pallet.locationId() != location.locationId())
|
||||
throw conflict("托盘不在目标库位或不可使用,请先完成托盘移库");
|
||||
palletCode = pallet.palletCode();
|
||||
}
|
||||
carton.setLocationCode(location.locationCode()); carton.setPalletCode(palletCode);
|
||||
carton.setPutawayAt(LocalDateTime.now()); carton.setUpdatedAt(carton.getPutawayAt()); cartons.updateById(carton);
|
||||
ReturnInventoryOperation operation = new ReturnInventoryOperation(); operation.setId(IdWorker.getId());
|
||||
operation.setOperatorId(identity.operatorKey()); operation.setIdempotencyKey(request.idempotencyKey()); operation.setRequestHash(digest);
|
||||
var result = new ReturnInventoryResponses.PutawayResult(carton.getCartonMark(), carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt());
|
||||
operation.setOperationType("PUTAWAY"); operation.setResultCartonId(carton.getId()); operation.setResultJson(json(result)); operation.setCreatedAt(LocalDateTime.now()); operation.setUpdatedAt(operation.getCreatedAt()); operations.insert(operation);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PackingPlan createPackingPlan(String operator, ReturnInventoryRequests.PackingPlanCreate request) {
|
||||
return createPackingPlan(operator, null, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PackingPlan createPackingPlan(String operator, String customerId, ReturnInventoryRequests.PackingPlanCreate request) {
|
||||
List<String> returnNos = request.returnNos().stream().map(this::requiredReturnNo).distinct().toList();
|
||||
List<ReturnOrder> selected = returnNos.stream().sorted().map(orders::lockNo).filter(java.util.Objects::nonNull).toList();
|
||||
if (selected.size() != returnNos.size()) throw notFound("存在不存在的退货单");
|
||||
ReturnOrder first = selected.get(0);
|
||||
if (customerId != null && !customerId.equals(first.getCustomerId())) throw notFound("退货单不存在");
|
||||
if (!"COMPLETED".equals(first.getStatus())) throw conflict("只能为已完成入库的退货单创建合箱计划");
|
||||
if (selected.stream().anyMatch(order -> !"COMPLETED".equals(order.getStatus()) || !first.getCustomerId().equals(order.getCustomerId()) || !first.getOverseasWarehouseId().equals(order.getOverseasWarehouseId()))) throw conflict("订单必须属于同一客户、同一海外仓且已完成入库");
|
||||
ReturnPackingPlan replay = packingPlans.lockCustomerKey(first.getCustomerId(), request.idempotencyKey());
|
||||
if (replay != null) {
|
||||
if (!hash(request).equals(replay.getRequestHash())) throw conflict("幂等键已用于不同的合箱计划");
|
||||
return packingPlan(replay);
|
||||
}
|
||||
String mode = required(request.mode(), "合箱方式不能为空").toUpperCase(java.util.Locale.ROOT);
|
||||
if (!List.of("BOX", "SKU").contains(mode)) throw badRequest("合箱方式只能选择 BOX 或 SKU");
|
||||
List<String> marks = request.sourceCartonMarks() == null ? List.of() : request.sourceCartonMarks().stream().distinct().sorted().toList();
|
||||
if ("BOX".equals(mode) && marks.isEmpty()) throw badRequest("按箱合箱必须选择来源箱唛");
|
||||
if ("SKU".equals(mode) && !marks.isEmpty()) throw badRequest("按 SKU 合箱不能同时选择来源箱");
|
||||
Map<String, Long> requested = planQuantities(request.boxes());
|
||||
List<SourceItem> allocation = new ArrayList<>();
|
||||
List<ReturnInventoryCarton> sources = "BOX".equals(mode)
|
||||
? marks.stream().map(cartons::lockMark).toList()
|
||||
: packingSourceCartons(selected).stream().filter(c -> "PARCEL".equals(c.getSourceUnitType()))
|
||||
.sorted(Comparator.comparing(ReturnInventoryCarton::getCartonMark)).map(c -> cartons.lockMark(c.getCartonMark())).toList();
|
||||
Map<String, Long> remaining = new LinkedHashMap<>(requested);
|
||||
for (ReturnInventoryCarton carton : sources) {
|
||||
if (carton == null || !first.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !first.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|
||||
|| !AVAILABLE.equals(carton.getStatus())
|
||||
|| !sourceMatches(carton, returnNos)) throw conflict("来源箱不属于所选退货单或已被占用");
|
||||
if ("BOX".equals(mode) && "PARCEL".equals(carton.getSourceUnitType())) throw badRequest("包裹散 SKU 不能作为整箱选择");
|
||||
for (ReturnInventoryCartonItem item : cartonItems.lockCarton(carton.getId())) {
|
||||
long free = Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity()));
|
||||
long take = "BOX".equals(mode) ? free : Math.min(free, value(remaining.get(item.getSkuCode())));
|
||||
if ("BOX".equals(mode) && value(item.getPackingReservedQuantity()) > 0) throw conflict("来源箱已有合箱预占");
|
||||
if (take <= 0) continue;
|
||||
if ("SKU".equals(mode)) remaining.put(item.getSkuCode(), remaining.get(item.getSkuCode()) - take);
|
||||
item.setPackingReservedQuantity(value(item.getPackingReservedQuantity()) + take);
|
||||
cartonItems.updateById(item);
|
||||
allocation.add(new SourceItem(carton.getCartonMark(), item.getSkuCode(), take));
|
||||
}
|
||||
if ("BOX".equals(mode)) { carton.setStatus("PACKING_RESERVED"); cartons.updateById(carton); }
|
||||
}
|
||||
if (allocation.isEmpty() || ("SKU".equals(mode) && remaining.values().stream().anyMatch(q -> q > 0))) throw conflict("来源库存不足,无法锁定合箱数量");
|
||||
Map<String, Long> reserved = sourceQuantities(allocation);
|
||||
for (Map.Entry<String, Long> entry : reserved.entrySet()) {
|
||||
if (balances.reserveAvailable(first.getCustomerId(), first.getOverseasWarehouseId(), entry.getKey(), entry.getValue()) != 1)
|
||||
throw conflict("SKU 可用库存不足,无法锁定合箱数量: " + entry.getKey());
|
||||
}
|
||||
List<ReturnInventoryRequests.PlanBox> boxes = List.of(new ReturnInventoryRequests.PlanBox(
|
||||
reserved.entrySet().stream().map(e -> new ReturnInventoryRequests.PlanItem(e.getKey(), e.getValue())).toList(), null, null, null, null));
|
||||
ReturnPackingPlan plan = new ReturnPackingPlan();
|
||||
plan.setId(IdWorker.getId()); plan.setPlanNo(numbers.next("RETURN_PACKING_PLAN", first.getOverseasWarehouseId(), plan.getId()));
|
||||
plan.setCustomerId(first.getCustomerId()); plan.setOverseasWarehouseId(first.getOverseasWarehouseId());
|
||||
plan.setReturnNosJson(json(returnNos)); plan.setBoxesJson(json(boxes)); plan.setSourceItemsJson(json(allocation));
|
||||
plan.setMode(mode); plan.setStatus("PENDING_REVIEW"); plan.setRemark(request.remark()); plan.setIdempotencyKey(request.idempotencyKey());
|
||||
plan.setRequestHash(hash(request));
|
||||
plan.setCreatedAt(LocalDateTime.now()); plan.setUpdatedAt(plan.getCreatedAt()); packingPlans.insert(plan);
|
||||
return packingPlan(plan);
|
||||
}
|
||||
|
||||
private record SourceItem(String cartonMark, String skuCode, long quantity) { }
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReturnOrderResponses.PackingDetail> packingHistory(String customerId, String returnNo) {
|
||||
return packingTrace.listReturn(customerId, returnNo).stream().filter(t -> StringUtils.hasText(t.sourceCartonMark()))
|
||||
.map(t -> new ReturnOrderResponses.PackingDetail(t.sourceCartonMark(), t.targetCartonMark(), t.returnNo(), t.skuCode(), t.quantity(), t.createdAt())).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PackingPlan cancelPackingPlan(String planNo) {
|
||||
ReturnPackingPlan plan = packingPlans.lockNo(required(planNo, "合箱计划号不能为空"));
|
||||
if (plan == null) throw notFound("合箱计划不存在");
|
||||
if ("CANCELLED".equals(plan.getStatus())) return packingPlan(plan);
|
||||
if (!List.of("PENDING_REVIEW", "READY").contains(plan.getStatus())) throw conflict("当前计划不能取消");
|
||||
List<SourceItem> allocation = read(plan.getSourceItemsJson(), new TypeReference<>() { });
|
||||
for (SourceItem source : allocation.stream().sorted(Comparator.comparing(SourceItem::cartonMark)).toList()) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(source.cartonMark());
|
||||
if (carton == null || !plan.getCustomerId().equals(carton.getCustomerId())) throw conflict("计划来源箱异常");
|
||||
var item = cartonItems.lockSku(carton.getId(), source.skuCode());
|
||||
if (item == null || value(item.getPackingReservedQuantity()) < source.quantity()) throw conflict("计划预占数量异常");
|
||||
item.setPackingReservedQuantity(value(item.getPackingReservedQuantity()) - source.quantity()); cartonItems.updateById(item);
|
||||
if ("PACKING_RESERVED".equals(carton.getStatus())) { carton.setStatus(AVAILABLE); cartons.updateById(carton); }
|
||||
}
|
||||
for (var entry : sourceQuantities(allocation).entrySet()) balances.adjust(IdWorker.getId(), plan.getCustomerId(), plan.getOverseasWarehouseId(), entry.getKey(), null, 0, entry.getValue(), 0);
|
||||
plan.setStatus("CANCELLED"); plan.setUpdatedAt(LocalDateTime.now()); packingPlans.updateById(plan);
|
||||
return packingPlan(plan);
|
||||
}
|
||||
|
||||
private List<ReturnInventoryCarton> packingSourceCartons(List<ReturnOrder> selected) {
|
||||
List<ReturnInventoryCarton> result = new ArrayList<>();
|
||||
for (ReturnOrder order : selected) for (ReturnReceipt receipt : receiptMapper.listOrder(order.getId()))
|
||||
result.addAll(cartons.listReceipt(receipt.getId()));
|
||||
// Merged cartons retain the actual source return numbers, even without a receipt ID.
|
||||
for (ReturnOrder order : selected) result.addAll(cartons.packingAvailable(order.getCustomerId(), order.getOverseasWarehouseId())
|
||||
.stream().filter(c -> sourceMatches(c, selected.stream().map(ReturnOrder::getReturnNo).toList())).toList());
|
||||
return new ArrayList<>(result.stream().collect(java.util.stream.Collectors.toMap(
|
||||
ReturnInventoryCarton::getCartonMark, c -> c, (a, b) -> a, LinkedHashMap::new)).values());
|
||||
}
|
||||
|
||||
private boolean sourceMatches(ReturnInventoryCarton carton, List<String> returnNos) {
|
||||
if (text(carton.getSourceReturnNo()) == null) return false;
|
||||
return java.util.Arrays.stream(carton.getSourceReturnNo().split(",")).allMatch(returnNos::contains);
|
||||
}
|
||||
|
||||
private Map<String, Long> planQuantities(List<ReturnInventoryRequests.PlanBox> boxes) {
|
||||
Map<String, Long> result = new LinkedHashMap<>();
|
||||
for (var box : boxes) if (box.items() != null) for (var item : box.items()) {
|
||||
if (item.quantity() <= 0) throw badRequest("合箱数量必须大于 0");
|
||||
result.merge(required(item.skuCode(), "SKU 不能为空"), item.quantity(), Math::addExact);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Long> sourceQuantities(List<SourceItem> items) {
|
||||
Map<String, Long> result = new LinkedHashMap<>();
|
||||
for (var item : items) result.merge(item.skuCode(), item.quantity(), Math::addExact);
|
||||
return result;
|
||||
}
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.PackingPlan> customerPackingPlans(String customerId, ReturnInventoryRequests.PackingPlanQuery request) {
|
||||
String status = text(request.status());
|
||||
long total = packingPlans.countCustomer(customerId, status);
|
||||
List<ReturnInventoryResponses.PackingPlan> records = total == 0 ? List.of() : packingPlans.listCustomer(customerId, status,
|
||||
(request.page() - 1) * request.size(), request.size()).stream().map(this::packingPlan).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.PackingPlan> platformPackingPlans(ReturnInventoryRequests.PackingPlanQuery request) {
|
||||
String status = text(request.status());
|
||||
String planNo = text(request.planNo());
|
||||
String customerId = text(request.customerId()), warehouseId = text(request.overseasWarehouseId()), sku = text(request.skuCode());
|
||||
long total = packingPlans.countPlatform(status, planNo, customerId, warehouseId, sku);
|
||||
List<ReturnInventoryResponses.PackingPlan> records = total == 0 ? List.of() : packingPlans.listPlatform(status, planNo, customerId, warehouseId, sku,
|
||||
(request.page() - 1) * request.size(), request.size()).stream().map(this::packingPlan).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PackingPlan reviewPackingPlanPlatform(String planNo) {
|
||||
ReturnPackingPlan plan = packingPlans.lockNo(required(planNo, "合箱计划号不能为空"));
|
||||
if (plan == null) throw notFound("合箱计划不存在");
|
||||
if (!"PENDING_REVIEW".equals(plan.getStatus())) throw conflict("当前合箱计划不在待审核状态");
|
||||
LocalDateTime now = LocalDateTime.now(); plan.setStatus("READY"); plan.setPushedAt(now); plan.setUpdatedAt(now); packingPlans.updateById(plan);
|
||||
return packingPlan(plan);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReturnInventoryResponses.PackingPlan> listPackingPlans(PdaWarehouseIdentity identity) {
|
||||
return packingPlans.listPending(String.valueOf(identity.overseasWarehouseId())).stream().map(this::packingPlan).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.PackingPlan packingPlan(PdaWarehouseIdentity identity, String planNo) {
|
||||
ReturnPackingPlan plan = packingPlans.findNo(required(planNo, "合箱计划号不能为空"));
|
||||
if (plan == null || plan.getOverseasWarehouseId() == null || !plan.getOverseasWarehouseId().equals(String.valueOf(identity.overseasWarehouseId()))) throw notFound("合箱计划不存在");
|
||||
return packingPlan(plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PackingPlanCompletion completePackingPlan(PdaWarehouseIdentity identity, String planNo, ReturnInventoryRequests.PackingPlanComplete request) {
|
||||
ReturnPackingPlan plan = packingPlans.lockNo(required(planNo, "合箱计划号不能为空"));
|
||||
if (plan == null || !String.valueOf(identity.overseasWarehouseId()).equals(plan.getOverseasWarehouseId())) throw notFound("合箱计划不存在");
|
||||
if ("COMPLETED".equals(plan.getStatus())) {
|
||||
if (!hash(request).equals(plan.getCompletionHash())) throw conflict("已完成计划不能使用不同的箱明细重复提交");
|
||||
return new ReturnInventoryResponses.PackingPlanCompletion(plan.getPlanNo(), read(plan.getResultLabelsJson(), new TypeReference<List<ReturnInventoryResponses.Label>>() { }));
|
||||
}
|
||||
if (!"READY".equals(plan.getStatus())) throw conflict("合箱计划尚未审核或已取消");
|
||||
if ("BOX".equals(plan.getMode()) && request.boxes().size() != 1) throw badRequest("箱与箱合并必须扫描一个已有目标箱,不支持拆分来源箱");
|
||||
List<SourceItem> allocation = read(plan.getSourceItemsJson(), new TypeReference<>() { });
|
||||
if (allocation.isEmpty()) throw conflict("合箱计划缺少来源锁定明细,请重新创建");
|
||||
String plannedTarget = "BOX".equals(plan.getMode()) && request.boxes().size() == 1
|
||||
? text(request.boxes().get(0).targetCartonMark()) : null;
|
||||
List<SourceItem> sourceAllocation = plannedTarget == null ? allocation : allocation.stream()
|
||||
.filter(source -> !plannedTarget.equals(source.cartonMark())).toList();
|
||||
if ("BOX".equals(plan.getMode()) && request.sourceDeductions() != null) {
|
||||
Map<String, Long> seen = new LinkedHashMap<>();
|
||||
List<SourceItem> selected = new ArrayList<>();
|
||||
for (var deduction : request.sourceDeductions()) {
|
||||
if (deduction.quantity() <= 0 || Objects.equals(plannedTarget, deduction.cartonMark())) throw badRequest("不能扣除目标箱库存");
|
||||
long maximum = sourceAllocation.stream().filter(s -> s.cartonMark().equals(deduction.cartonMark()) && s.skuCode().equals(deduction.skuCode())).mapToLong(SourceItem::quantity).sum();
|
||||
if (deduction.quantity() > maximum || seen.put(deduction.cartonMark() + ":" + deduction.skuCode(), deduction.quantity()) != null) throw conflict("来源箱 SKU 扣除数量超过计划或重复");
|
||||
selected.add(new SourceItem(deduction.cartonMark(), deduction.skuCode(), deduction.quantity()));
|
||||
}
|
||||
sourceAllocation = selected;
|
||||
}
|
||||
final List<SourceItem> selectedAllocation = sourceAllocation;
|
||||
if (!sourceQuantities(sourceAllocation).equals(planQuantities(request.boxes()))) throw conflict("来源箱 SKU 数量合计必须与计划锁定数量完全一致");
|
||||
List<String> returnNos = read(plan.getReturnNosJson(), new TypeReference<>() { });
|
||||
Map<String, Map<String, Long>> limits = new LinkedHashMap<>();
|
||||
for (SourceItem source : allocation.stream().filter(source -> !Objects.equals(plannedTarget, source.cartonMark())).sorted(Comparator.comparing(SourceItem::cartonMark)).toList()) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(source.cartonMark());
|
||||
if (carton == null || !plan.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !List.of(AVAILABLE, "PACKING_RESERVED").contains(carton.getStatus())) throw conflict("计划来源箱状态异常");
|
||||
ReturnInventoryCartonItem item = cartonItems.lockSku(carton.getId(), source.skuCode());
|
||||
if (item == null || value(item.getPackingReservedQuantity()) < source.quantity()) throw conflict("计划库存预占丢失");
|
||||
item.setPackingReservedQuantity(value(item.getPackingReservedQuantity()) - source.quantity());
|
||||
cartonItems.updateById(item);
|
||||
if ("PACKING_RESERVED".equals(carton.getStatus())) { carton.setStatus(AVAILABLE); cartons.updateById(carton); }
|
||||
long selected = selectedAllocation.stream().filter(s -> s.cartonMark().equals(source.cartonMark()) && s.skuCode().equals(source.skuCode())).mapToLong(SourceItem::quantity).sum();
|
||||
if (selected > 0) limits.computeIfAbsent(source.cartonMark(), ignored -> new LinkedHashMap<>()).merge(source.skuCode(), selected, Math::addExact);
|
||||
}
|
||||
if (plannedTarget != null) {
|
||||
ReturnInventoryCarton targetSource = cartons.lockMark(plannedTarget);
|
||||
if (targetSource == null || !plan.getCustomerId().equals(targetSource.getCustomerId())
|
||||
|| !List.of(AVAILABLE, "PACKING_RESERVED").contains(targetSource.getStatus())) throw conflict("目标原箱状态异常");
|
||||
for (SourceItem source : allocation.stream().filter(item -> plannedTarget.equals(item.cartonMark())).toList()) {
|
||||
ReturnInventoryCartonItem item = cartonItems.lockSku(targetSource.getId(), source.skuCode());
|
||||
if (item == null || value(item.getPackingReservedQuantity()) < source.quantity()) throw conflict("目标原箱库存预占丢失");
|
||||
item.setPackingReservedQuantity(value(item.getPackingReservedQuantity()) - source.quantity()); cartonItems.updateById(item);
|
||||
}
|
||||
if ("PACKING_RESERVED".equals(targetSource.getStatus())) { targetSource.setStatus(AVAILABLE); cartons.updateById(targetSource); }
|
||||
}
|
||||
for (var entry : sourceQuantities(allocation).entrySet()) balances.adjust(IdWorker.getId(), plan.getCustomerId(),
|
||||
plan.getOverseasWarehouseId(), entry.getKey(), null, 0, entry.getValue(), 0);
|
||||
List<ReturnInventoryResponses.Label> labels = new ArrayList<>();
|
||||
List<String> generatedMarks = "SKU".equals(plan.getMode()) || plannedTarget == null ? packingMarks(request.boxes()) : Collections.nCopies(request.boxes().size(), null);
|
||||
int index = 0;
|
||||
for (ReturnInventoryRequests.PlanBox box : request.boxes()) {
|
||||
if (box.items() == null || box.items().isEmpty()) throw badRequest("每个目标箱至少填写一条 SKU");
|
||||
if (( !"BOX".equals(plan.getMode()) || plannedTarget == null) && (box.length() == null || box.width() == null || box.height() == null || box.weight() == null
|
||||
|| box.length().signum() <= 0 || box.width().signum() <= 0 || box.height().signum() <= 0 || box.weight().signum() <= 0))
|
||||
throw badRequest("PDA 完成合箱时必须填写有效箱规");
|
||||
List<ReturnInventoryRequests.ValidItem> items = box.items().stream().map(item -> new ReturnInventoryRequests.ValidItem(item.skuCode(), item.quantity())).toList();
|
||||
var result = mergeScoped(identity, new ReturnInventoryRequests.Merge(returnNos, items, box.length(), box.width(), box.height(), box.weight(),
|
||||
request.idempotencyKey() + "-" + index), limits,
|
||||
"BOX".equals(plan.getMode()) ? text(box.targetCartonMark()) : null, generatedMarks.get(index++));
|
||||
labels.add(result.label());
|
||||
ReturnInventoryOperation completedOperation = operations.lockKey(identity.operatorKey(), request.idempotencyKey() + "-" + (index - 1));
|
||||
if (completedOperation != null) {
|
||||
completedOperation.setResultJson(json(new ReturnInventoryResponses.MergeResult(result.cartonMark(), result.skuCode(), result.totalQuantity(), result.label(), plan.getPlanNo())));
|
||||
operations.updateById(completedOperation);
|
||||
}
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(); plan.setStatus("COMPLETED"); plan.setUpdatedAt(now); plan.setCompletedAt(now);
|
||||
plan.setBoxesJson(json(request.boxes())); plan.setResultMarksJson(json(labels.stream().map(ReturnInventoryResponses.Label::cartonMark).toList()));
|
||||
plan.setCompletionHash(hash(request));
|
||||
plan.setResultLabelsJson(json(labels));
|
||||
packingPlans.updateById(plan);
|
||||
return new ReturnInventoryResponses.PackingPlanCompletion(plan.getPlanNo(), labels);
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.PackingPlan packingPlan(ReturnPackingPlan plan) {
|
||||
List<SourceItem> sources = read(plan.getSourceItemsJson(), new TypeReference<>() { });
|
||||
List<ReturnInventoryRequests.PlanBox> boxes = read(plan.getBoxesJson(), new TypeReference<>() { });
|
||||
List<ReturnInventoryResponses.PlanBox> responseBoxes = new ArrayList<>();
|
||||
for (int i = 0; i < boxes.size(); i++) {
|
||||
var box = boxes.get(i);
|
||||
responseBoxes.add(new ReturnInventoryResponses.PlanBox(i + 1, box.items() == null ? List.of() : box.items().stream()
|
||||
.map(item -> new ReturnInventoryResponses.PlanItem(item.skuCode(), item.quantity())).toList(), box.length(), box.width(), box.height(), box.weight()));
|
||||
}
|
||||
return new ReturnInventoryResponses.PackingPlan(plan.getPlanNo(), plan.getCustomerId(), plan.getOverseasWarehouseId(),
|
||||
read(plan.getReturnNosJson(), new TypeReference<>() { }), plan.getMode(), plan.getStatus(), responseBoxes, plan.getRemark(), plan.getPushedAt(),
|
||||
sources.stream().map(SourceItem::cartonMark).distinct().toList(),
|
||||
sources.stream().map(SourceItem::cartonMark).distinct().map(mark -> new ReturnInventoryResponses.PlanSource(mark,
|
||||
sources.stream().filter(source -> source.cartonMark().equals(mark)).map(source -> new ReturnInventoryResponses.PlanItem(source.skuCode(), source.quantity())).toList())).toList(),
|
||||
plan.getCreatedAt(), "COMPLETED".equals(plan.getStatus())
|
||||
? read(plan.getResultLabelsJson(), new TypeReference<List<ReturnInventoryResponses.Label>>() { }).stream()
|
||||
.map(label -> cartons.findCustomerMark(plan.getCustomerId(), label.cartonMark()))
|
||||
.filter(java.util.Objects::nonNull).map(this::cartonDetail).toList() : List.of(), plan.getCompletedAt());
|
||||
}
|
||||
private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception error) { throw new IllegalStateException("合箱计划序列化失败", error); } }
|
||||
private <T> T read(String value, TypeReference<T> type) { try { return objectMapper.readValue(value == null ? "[]" : value, type); } catch (Exception error) { throw new IllegalStateException("合箱计划数据损坏", error); } }
|
||||
|
||||
/** Called inside the PDA pick transaction; carton contents remain the physical shipment snapshot. */
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
@@ -66,14 +383,15 @@ public class ReturnInventoryService {
|
||||
String outboundNo, long outboundPickId) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(required(cartonMark, "退货箱码不能为空"));
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !warehouseId.equals(carton.getOverseasWarehouseId()) || !returnNo.equals(carton.getSourceReturnNo())
|
||||
|| !AVAILABLE.equals(carton.getStatus())) throw conflict("退货箱状态或客户、仓库、来源不匹配");
|
||||
|| !warehouseId.equals(carton.getOverseasWarehouseId()) || !matchesReturnSource(carton, returnNo)
|
||||
|| !"OUTBOUND_PENDING".equals(carton.getStatus())) throw conflict("退货箱状态或客户、仓库、来源不匹配");
|
||||
List<ReturnInventoryCartonItem> contents = cartonItems.lockCarton(carton.getId());
|
||||
long total = 0;
|
||||
var skus = new java.util.HashSet<String>();
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
if (!customerId.equals(content.getCustomerId()) || !carton.getId().equals(content.getCartonId())
|
||||
|| text(content.getSkuCode()) == null || !skus.add(content.getSkuCode()) || value(content.getQuantity()) <= 0) {
|
||||
|| text(content.getSkuCode()) == null || !skus.add(content.getSkuCode()) || value(content.getQuantity()) <= 0
|
||||
|| value(content.getPackingReservedQuantity()) > 0) {
|
||||
throw conflict("退货箱内容缺失或归属、SKU数量不一致");
|
||||
}
|
||||
total = Math.addExact(total, content.getQuantity());
|
||||
@@ -117,8 +435,7 @@ public class ReturnInventoryService {
|
||||
public ReturnInventoryCarton createReceiptCarton(ReturnOrder order, ReturnReceipt receipt,
|
||||
String requestedMark, List<ReceiptLine> lines) {
|
||||
if (lines.isEmpty()) throw new IllegalArgumentException("退货签收库存明细不能为空");
|
||||
if (lines.size() != 1) throw conflict("一个签收批次只能签收一个 SKU");
|
||||
String normalizedPalletCode = required(receipt.getPalletCode(), "托盘码不能为空");
|
||||
String normalizedUnitCode = required(receipt.getUnitCode(), "实物单元编码不能为空");
|
||||
required(lines.get(0).skuCode(), "SKU 不能为空");
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
@@ -130,7 +447,13 @@ public class ReturnInventoryService {
|
||||
carton.setId(cartonId); carton.setCustomerId(order.getCustomerId()); carton.setOverseasWarehouseId(order.getOverseasWarehouseId());
|
||||
carton.setSourceReceiptId(receipt.getId()); carton.setSourceBatchNo(receipt.getReceiptBatchNo());
|
||||
carton.setSourceReturnNo(order.getReturnNo()); carton.setCartonMark(cartonMark);
|
||||
carton.setPalletCode(normalizedPalletCode);
|
||||
carton.setPalletCode(text(receipt.getPalletCode()));
|
||||
carton.setSourceUnitType(receipt.getUnitType());
|
||||
carton.setSourceUnitCode(normalizedUnitCode);
|
||||
// Pallet measurements describe the carrier, never each child carton.
|
||||
if ("BOX".equals(receipt.getUnitType())) {
|
||||
carton.setLength(receipt.getLength()); carton.setWidth(receipt.getWidth()); carton.setHeight(receipt.getHeight()); carton.setWeight(receipt.getWeight());
|
||||
}
|
||||
carton.setStatus(AVAILABLE); carton.setSkuTypes(lines.size()); carton.setTotalQuantity(total); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
for (ReceiptLine line : lines) {
|
||||
@@ -144,6 +467,33 @@ public class ReturnInventoryService {
|
||||
return carton;
|
||||
}
|
||||
|
||||
/** 托盘仅作为载体;按 SKU 箱数生成独立的退货库存箱。 */
|
||||
@Transactional
|
||||
public List<ReturnInventoryCarton> createReceiptCartons(ReturnOrder order, ReturnReceipt receipt,
|
||||
List<ReceiptLine> lines,
|
||||
Map<String, Long> boxCounts) {
|
||||
List<ReturnInventoryCarton> result = new ArrayList<>();
|
||||
for (ReceiptLine line : lines) {
|
||||
long boxes = boxCounts.getOrDefault(line.skuCode(), 0L);
|
||||
if (boxes <= 0) throw badRequest("托盘签收的 SKU 箱数必须大于 0: " + line.skuCode());
|
||||
long base = line.quantity() / boxes;
|
||||
long remainder = line.quantity() % boxes;
|
||||
for (long index = 0; index < boxes; index++) {
|
||||
long quantity = base + (index < remainder ? 1 : 0);
|
||||
if (quantity <= 0) throw badRequest("SKU 数量不足以分配到箱: " + line.skuCode());
|
||||
String mark = shortReturnMark(order.getReturnNo(), receipt.getId(), line.skuCode(), boxes, index + 1);
|
||||
result.add(createReceiptCarton(order, receipt, mark,
|
||||
List.of(new ReceiptLine(line.skuCode(), line.productNameZh(), quantity))));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String shortReturnMark(String returnNo, Long receiptId, String skuCode, long totalBoxes, long sequence) {
|
||||
String prefix = hash(java.util.Arrays.asList(returnNo, receiptId, skuCode)).substring(0, 16).toUpperCase(java.util.Locale.ROOT);
|
||||
return "RC" + prefix + "#" + totalBoxes + "-" + sequence;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.InventoryItem> page(String customerId, ReturnInventoryRequests.Query request) {
|
||||
String warehouseId = text(request.overseasWarehouseId());
|
||||
@@ -185,7 +535,8 @@ public class ReturnInventoryService {
|
||||
List<ReturnInventoryResponses.PlatformStock> records = total == 0 ? List.of() : balances.platformPage(customerId, warehouseId, sku,
|
||||
(request.page() - 1) * request.size(), request.size()).stream().map(balance -> new ReturnInventoryResponses.PlatformStock(
|
||||
balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(),
|
||||
value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt())).toList();
|
||||
value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt(),
|
||||
cartonItems.looseAvailableQuantity(balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode()))).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@@ -205,9 +556,9 @@ public class ReturnInventoryService {
|
||||
long total = cartons.platformCount(customerId, warehouseId, cartonMark, returnNo, status, sku);
|
||||
List<ReturnInventoryResponses.PlatformCarton> records = total == 0 ? List.of() : cartons.platformPage(customerId, warehouseId, cartonMark,
|
||||
returnNo, status, sku, (request.page() - 1) * request.size(), request.size()).stream().map(carton -> new ReturnInventoryResponses.PlatformCarton(
|
||||
carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getSourceBatchNo(),
|
||||
carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getSourceUnitType(), carton.getSourceBatchNo(),
|
||||
carton.getStatus(), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), carton.getRelabelStatus(), carton.getLength(),
|
||||
carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt())).toList();
|
||||
carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt())).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@@ -218,6 +569,24 @@ public class ReturnInventoryService {
|
||||
return cartonDetail(carton);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<ReturnInventoryResponses.PackingSource> platformPackingSourcePage(ReturnInventoryRequests.PlatformQuery request) {
|
||||
String customerId = required(request.customerId(), "客户不能为空");
|
||||
String warehouseId = required(request.overseasWarehouseId(), "海外仓不能为空");
|
||||
long total = cartons.platformCount(customerId, warehouseId, null, null, AVAILABLE, null);
|
||||
List<ReturnInventoryCarton> page = total == 0 ? List.of() : cartons.platformPage(customerId, warehouseId,
|
||||
null, null, AVAILABLE, null, (request.page() - 1) * request.size(), request.size());
|
||||
java.util.Map<Long, List<ReturnInventoryCartonItem>> itemsByCarton = page.isEmpty() ? java.util.Map.of() :
|
||||
cartonItems.listCartons(page.stream().map(ReturnInventoryCarton::getId).toList()).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(ReturnInventoryCartonItem::getCartonId));
|
||||
List<ReturnInventoryResponses.PackingSource> records = page.stream().map(carton ->
|
||||
new ReturnInventoryResponses.PackingSource(carton.getCartonMark(), carton.getSourceReturnNo(), carton.getSourceUnitType(),
|
||||
itemsByCarton.getOrDefault(carton.getId(), List.of()).stream().map(item ->
|
||||
new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), value(item.getQuantity()),
|
||||
Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity())))).toList())).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.Label platformLabel(String cartonMark) {
|
||||
ReturnInventoryCarton carton = cartons.findMark(required(cartonMark, "箱号不能为空"));
|
||||
@@ -246,33 +615,228 @@ public class ReturnInventoryService {
|
||||
return label(carton);
|
||||
}
|
||||
|
||||
/** Print only persisted physical cartons; a parcel's SKU rows are not cartons. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReturnOrderResponses.CartonLabel> receiptLabels(ReturnOrder order, ReturnReceipt receipt,
|
||||
List<ReturnOrderLine> lines) {
|
||||
List<ReturnInventoryCarton> existing = cartons.listReceipt(receipt.getId()).stream()
|
||||
.filter(carton -> !"MERGED".equals(carton.getStatus())).toList();
|
||||
if ("PARCEL".equals(receipt.getUnitType())) return List.of();
|
||||
long expected = "BOX".equals(receipt.getUnitType()) ? Math.max(1, value(receipt.getTotalBoxCount()))
|
||||
: lines.stream().mapToLong(line -> Math.max(0, line.boxCount())).sum();
|
||||
if (expected <= 0 || existing.size() >= expected) {
|
||||
return existing.stream().map(carton -> new ReturnOrderResponses.CartonLabel(carton.getCartonMark(), label(order.getCustomerId(), carton.getCartonMark()).printableHtml())).toList();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ReturnOrderResponses.CartonLabel> repairReceiptCartons(String operator, String customerId,
|
||||
String returnNo, String receiptNo, ReturnInventoryRequests.ReceiptCartonRepair request) {
|
||||
ReturnOrder order = orders.lockCustomerNo(customerId, required(returnNo, "退货单不能为空"));
|
||||
if (order == null) throw notFound("退货单不存在");
|
||||
ReturnReceipt receipt = receiptMapper.listOrder(order.getId()).stream()
|
||||
.filter(value -> receiptNo.equals(value.getReceiptNo())).findFirst().orElseThrow(() -> notFound("签收记录不存在"));
|
||||
String digest = hash(List.of(customerId, returnNo, receiptNo, request));
|
||||
ReturnInventoryOperation replay = operations.lockKey(operator, request.idempotencyKey());
|
||||
if (replay != null) {
|
||||
if (!"REPAIR_RECEIPT".equals(replay.getOperationType()) || !digest.equals(replay.getRequestHash()))
|
||||
throw conflict("幂等键已用于不同的历史箱修复请求");
|
||||
return read(replay.getResultJson(), new TypeReference<List<ReturnOrderResponses.CartonLabel>>() { });
|
||||
}
|
||||
var lines = receiptItemMapper.listReceipt(receipt.getId());
|
||||
long expected = lines.stream().mapToLong(line -> value(line.getBoxCount())).sum();
|
||||
if (!"PALLET".equals(receipt.getUnitType()) || expected <= 1 || request.confirmedBoxCount() != expected)
|
||||
throw conflict("只支持现场核实总箱数后的历史整托签收修复");
|
||||
List<ReturnInventoryCarton> original = cartons.listReceipt(receipt.getId());
|
||||
if (original.size() != 1) throw conflict("仅可修复旧版一条整托库存记录,其他情况需要逐箱核实");
|
||||
ReturnInventoryCarton source = cartons.lockId(original.get(0).getId());
|
||||
if (!customerId.equals(source.getCustomerId()) || !returnNo.equals(source.getSourceReturnNo())
|
||||
|| !AVAILABLE.equals(source.getStatus()) || source.getRelabelApplicationId() != null || source.getPutawayAt() != null)
|
||||
throw conflict("历史箱已被作业占用或上架,不能自动拆分修复");
|
||||
var contents = cartonItems.lockCarton(source.getId()).stream().sorted(Comparator.comparing(ReturnInventoryCartonItem::getSkuCode)).toList();
|
||||
var counts = new LinkedHashMap<String, Long>();
|
||||
var quantities = new LinkedHashMap<String, Long>();
|
||||
for (var line : lines) { counts.merge(line.getSkuCode(), value(line.getBoxCount()), Math::addExact); quantities.merge(line.getSkuCode(), value(line.getReceivedQuantity()), Math::addExact); }
|
||||
if (contents.size() != counts.size() || value(source.getTotalQuantity()) != value(receipt.getReceivedQuantity()))
|
||||
throw conflict("历史实收与当前箱内容不一致,需人工核对");
|
||||
for (var item : contents) {
|
||||
long quantity = value(item.getQuantity()), boxes = counts.getOrDefault(item.getSkuCode(), 0L);
|
||||
if (quantity != quantities.getOrDefault(item.getSkuCode(), 0L) || boxes <= 0 || boxes > quantity
|
||||
|| value(item.getPackingReservedQuantity()) > 0
|
||||
|| !packingTrace.ownership(customerId, source.getCartonMark(), item.getSkuCode()).isEmpty())
|
||||
throw conflict("历史箱SKU数量、箱数或来源已变化,不能自动修复");
|
||||
var balance = balances.lockSku(customerId, order.getOverseasWarehouseId(), item.getSkuCode());
|
||||
if (balance == null || value(balance.getAvailableQuantity()) < quantity || value(balance.getCartonQuantity()) < 1)
|
||||
throw conflict("历史库存余额不足,不能自动修复");
|
||||
}
|
||||
long operationId = IdWorker.getId();
|
||||
List<ReceiptLine> replacement = contents.stream().map(item -> new ReceiptLine(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList();
|
||||
for (var item : contents) {
|
||||
balances.adjust(IdWorker.getId(), customerId, order.getOverseasWarehouseId(), item.getSkuCode(), item.getProductNameZh(), -item.getQuantity(), -item.getQuantity(), -1);
|
||||
item.setQuantity(0L); cartonItems.updateById(item);
|
||||
}
|
||||
source.setStatus("MERGED"); source.setTotalQuantity(0L); source.setSkuTypes(0); source.setUpdatedAt(LocalDateTime.now()); cartons.updateById(source);
|
||||
var generated = createReceiptCartons(order, receipt, replacement, counts);
|
||||
for (var carton : generated) for (var item : cartonItems.listCarton(carton.getId()))
|
||||
packingTrace.insert(IdWorker.getId(), customerId, operationId, source.getCartonMark(), carton.getCartonMark(), returnNo,
|
||||
item.getSkuCode(), item.getQuantity(), LocalDateTime.now());
|
||||
receipt.setCartonMark(generated.get(0).getCartonMark()); receiptMapper.updateById(receipt);
|
||||
List<ReturnOrderResponses.CartonLabel> result = generated.stream().map(carton -> new ReturnOrderResponses.CartonLabel(
|
||||
carton.getCartonMark(), label(customerId, carton.getCartonMark()).printableHtml())).toList();
|
||||
ReturnInventoryOperation operation = new ReturnInventoryOperation(); operation.setId(operationId);
|
||||
operation.setOperatorId(operator); operation.setIdempotencyKey(request.idempotencyKey()); operation.setRequestHash(digest);
|
||||
operation.setOperationType("REPAIR_RECEIPT"); operation.setResultCartonId(source.getId()); operation.setResultJson(json(result));
|
||||
operation.setCreatedAt(LocalDateTime.now()); operation.setUpdatedAt(operation.getCreatedAt()); operations.insert(operation);
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean receiptRepairRequired(ReturnReceipt receipt, long expected) {
|
||||
if ("PARCEL".equals(receipt.getUnitType()) || expected <= 0) return false;
|
||||
// Consumed/merged originals are valid history, not missing physical box records.
|
||||
return cartons.listReceipt(receipt.getId()).size() < expected;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.PackingPreview packingPreview(PdaWarehouseIdentity identity, ReturnInventoryRequests.PackingQuery request) {
|
||||
List<ReturnOrder> candidates = orders.findCompletedLike(String.valueOf(identity.overseasWarehouseId()), text(request.returnNo()) == null ? "" : text(request.returnNo()));
|
||||
List<ReturnOrder> candidates;
|
||||
if (text(request.skuCode()) != null) {
|
||||
List<ReturnOrder> matching = orderItems.completedSkuCandidates(String.valueOf(identity.overseasWarehouseId()), text(request.skuCode())).stream()
|
||||
.map(item -> orders.selectById(item.getReturnOrderId())).filter(java.util.Objects::nonNull).toList();
|
||||
candidates = matching.stream().map(ReturnOrder::getCustomerId).distinct()
|
||||
.flatMap(customer -> orders.completedCustomer(customer, String.valueOf(identity.overseasWarehouseId())).stream()).toList();
|
||||
} else {
|
||||
candidates = orders.findCompletedLike(String.valueOf(identity.overseasWarehouseId()), text(request.returnNo()) == null ? "" : text(request.returnNo()));
|
||||
}
|
||||
Map<String, ReturnOrder> unique = new LinkedHashMap<>();
|
||||
candidates.forEach(order -> unique.put(order.getReturnNo(), order));
|
||||
List<ReturnOrder> selected = unique.values().stream().limit(100).toList();
|
||||
String customerId = selected.size() == 1 ? selected.get(0).getCustomerId() : null;
|
||||
Map<String, Long> quantities = new LinkedHashMap<>();
|
||||
Map<String, String> names = new LinkedHashMap<>();
|
||||
for (ReturnOrder order : selected) {
|
||||
List<ReturnOrder> selected = unique.values().stream().toList();
|
||||
Map<String, List<ReturnOrder>> byCustomer = selected.stream().collect(java.util.stream.Collectors.groupingBy(ReturnOrder::getCustomerId, LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
List<ReturnInventoryResponses.PackingCustomer> customers = new ArrayList<>();
|
||||
for (Map.Entry<String, List<ReturnOrder>> customer : byCustomer.entrySet()) {
|
||||
Map<String, Long> quantities = new LinkedHashMap<>(); Map<String, String> names = new LinkedHashMap<>();
|
||||
LinkedHashSet<String> ordersWithAvailableSku = new LinkedHashSet<>();
|
||||
for (ReturnOrder order : customer.getValue()) {
|
||||
for (ReturnReceipt receipt : receiptMapper.listOrder(order.getId())) {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
if (carton == null || !AVAILABLE.equals(carton.getStatus())) continue;
|
||||
for (ReturnInventoryCartonItem item : cartonItems.listCarton(carton.getId())) {
|
||||
quantities.merge(item.getSkuCode(), value(item.getQuantity()), Math::addExact);
|
||||
names.putIfAbsent(item.getSkuCode(), item.getProductNameZh());
|
||||
for (ReturnInventoryCarton carton : cartons.listReceipt(receipt.getId())) {
|
||||
if (!AVAILABLE.equals(carton.getStatus()) || !"PARCEL".equals(carton.getSourceUnitType())) continue;
|
||||
for (ReturnInventoryCartonItem item : cartonItems.listCarton(carton.getId())) {
|
||||
if (request.skuCode() != null && text(request.skuCode()).equals(item.getSkuCode()) && value(item.getQuantity()) - value(item.getPackingReservedQuantity()) > 0) {
|
||||
ordersWithAvailableSku.add(order.getReturnNo());
|
||||
}
|
||||
quantities.merge(item.getSkuCode(), Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity())), Math::addExact);
|
||||
names.putIfAbsent(item.getSkuCode(), item.getProductNameZh());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
quantities.replaceAll((sku, quantity) -> Math.min(quantity, balances.availableQuantity(customer.getKey(), String.valueOf(identity.overseasWarehouseId()), sku)));
|
||||
if (text(request.skuCode()) != null && ordersWithAvailableSku.isEmpty()) continue;
|
||||
List<ReturnOrder> customerOrders = customer.getValue();
|
||||
customers.add(new ReturnInventoryResponses.PackingCustomer(customer.getKey(), customerOrders.stream().map(order -> new ReturnInventoryResponses.PackingOrder(order.getReturnNo(), order.getCustomerId(), order.getCustomerReferenceNo(), order.getStatus())).toList(), quantities.entrySet().stream().filter(entry -> entry.getValue() > 0).map(entry -> new ReturnInventoryResponses.PackingSku(entry.getKey(), names.get(entry.getKey()), entry.getValue())).toList()));
|
||||
}
|
||||
return new ReturnInventoryResponses.PackingPreview(selected.stream().map(order ->
|
||||
new ReturnInventoryResponses.PackingOrder(order.getReturnNo(), order.getCustomerId(), order.getCustomerReferenceNo(), order.getStatus())).toList(),
|
||||
customerId, quantities.entrySet().stream().map(entry -> new ReturnInventoryResponses.PackingSku(entry.getKey(), names.get(entry.getKey()), entry.getValue())).toList());
|
||||
String customerId = customers.size() == 1 ? customers.get(0).customerId() : null;
|
||||
ReturnInventoryResponses.PackingCustomer first = customers.size() == 1 ? customers.get(0) : null;
|
||||
return new ReturnInventoryResponses.PackingPreview(selected.stream().map(order -> new ReturnInventoryResponses.PackingOrder(order.getReturnNo(), order.getCustomerId(), order.getCustomerReferenceNo(), order.getStatus())).toList(), customerId, first == null ? List.of() : first.skus(), customers);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.MergeResult merge(PdaWarehouseIdentity identity, ReturnInventoryRequests.Merge request) {
|
||||
String requestHash = hash(request);
|
||||
return mergeScoped(identity, request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.PackingPlanCompletion pack(PdaWarehouseIdentity identity, ReturnInventoryRequests.PackingExecution request) {
|
||||
String digest = hash(request);
|
||||
for (String returnNo : request.returnNos().stream().distinct().sorted().toList()) {
|
||||
var order = orders.lockNo(returnNo);
|
||||
if (order == null || !String.valueOf(identity.overseasWarehouseId()).equals(order.getOverseasWarehouseId())) throw notFound("当前仓没有来源退货单");
|
||||
}
|
||||
ReturnInventoryOperation replay = operations.lockKey(identity.operatorKey(), request.idempotencyKey());
|
||||
if (replay != null && (!"PACK".equals(replay.getOperationType()) || !digest.equals(replay.getRequestHash()))) throw conflict("幂等键已用于不同的多箱合箱请求");
|
||||
if (replay != null) return read(replay.getResultJson(), new TypeReference<ReturnInventoryResponses.PackingPlanCompletion>() { });
|
||||
List<ReturnInventoryResponses.Label> labels = new ArrayList<>();
|
||||
List<String> generatedMarks = packingMarks(request.boxes());
|
||||
int index = 0;
|
||||
for (var box : request.boxes()) {
|
||||
if (text(box.targetCartonMark()) != null) throw badRequest("散 SKU 合箱不能同时指定已有目标箱");
|
||||
if (box.items() == null || box.items().isEmpty() || box.length() == null || box.width() == null || box.height() == null || box.weight() == null
|
||||
|| box.length().signum() <= 0 || box.width().signum() <= 0 || box.height().signum() <= 0 || box.weight().signum() <= 0) throw badRequest("每个新箱都必须填写 SKU、数量和有效箱规");
|
||||
var result = mergeScoped(identity, new ReturnInventoryRequests.Merge(request.returnNos(), box.items().stream().map(i -> new ReturnInventoryRequests.ValidItem(i.skuCode(), i.quantity())).toList(),
|
||||
box.length(), box.width(), box.height(), box.weight(), request.idempotencyKey() + ":box:" + index), null, null, generatedMarks.get(index++));
|
||||
labels.add(result.label());
|
||||
}
|
||||
if (replay == null) {
|
||||
ReturnInventoryOperation operation = new ReturnInventoryOperation(); operation.setId(IdWorker.getId()); operation.setOperatorId(identity.operatorKey());
|
||||
operation.setIdempotencyKey(request.idempotencyKey()); operation.setRequestHash(digest); operation.setOperationType("PACK");
|
||||
operation.setResultJson(json(new ReturnInventoryResponses.PackingPlanCompletion(null, labels)));
|
||||
operation.setCreatedAt(LocalDateTime.now()); operation.setUpdatedAt(operation.getCreatedAt()); operations.insert(operation);
|
||||
}
|
||||
return new ReturnInventoryResponses.PackingPlanCompletion(null, labels);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnInventoryResponses.CartonDetail scanCarton(PdaWarehouseIdentity identity, String mark) {
|
||||
ReturnInventoryCarton carton = cartons.findMark(required(mark, "箱唛不能为空"));
|
||||
if (carton == null || !String.valueOf(identity.overseasWarehouseId()).equals(carton.getOverseasWarehouseId())) throw notFound("当前仓没有该退货箱");
|
||||
if (!AVAILABLE.equals(carton.getStatus()) || "PARCEL".equals(carton.getSourceUnitType())) throw conflict("该箱不是可用实物箱");
|
||||
return cartonDetail(carton);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReturnInventoryResponses.MergeResult mergeBoxes(PdaWarehouseIdentity identity, ReturnInventoryRequests.BoxMerge request) {
|
||||
// Bind replay to the complete scan command before looking up emptied sources.
|
||||
ReturnInventoryOperation replay = operations.lockKey(identity.operatorKey(), request.idempotencyKey());
|
||||
String digest = hash(request);
|
||||
if (replay != null) return replayMerge(replay, digest);
|
||||
java.util.stream.Stream.concat(request.sourceCartonMarks().stream(), java.util.stream.Stream.of(request.targetCartonMark()))
|
||||
.filter(StringUtils::hasText).distinct().sorted().forEach(cartons::lockMark);
|
||||
Map<String, Map<String, Long>> limits = new LinkedHashMap<>();
|
||||
Map<String, Long> quantities = new LinkedHashMap<>();
|
||||
LinkedHashSet<String> returnNos = new LinkedHashSet<>();
|
||||
String customer = null;
|
||||
for (String mark : request.sourceCartonMarks().stream().distinct().sorted().toList()) {
|
||||
if (mark.equals(request.targetCartonMark())) throw badRequest("来源箱不能与目标箱相同");
|
||||
ReturnInventoryCarton carton = cartons.lockMark(mark);
|
||||
if (carton == null || !AVAILABLE.equals(carton.getStatus()) || "PARCEL".equals(carton.getSourceUnitType())
|
||||
|| !String.valueOf(identity.overseasWarehouseId()).equals(carton.getOverseasWarehouseId())) throw conflict("来源箱不存在或不可合并");
|
||||
if (customer != null && !customer.equals(carton.getCustomerId())) throw conflict("不同客户的箱不能合并");
|
||||
customer = carton.getCustomerId(); returnNos.addAll(java.util.Arrays.asList(required(carton.getSourceReturnNo(), "来源退货单缺失").split(",")));
|
||||
Map<String, Long> items = new LinkedHashMap<>();
|
||||
for (var item : cartonItems.lockCarton(carton.getId())) {
|
||||
if (value(item.getPackingReservedQuantity()) > 0) throw conflict("来源箱已被合箱计划占用");
|
||||
long selected = request.sourceDeductions() == null ? value(item.getQuantity()) : request.sourceDeductions().stream()
|
||||
.filter(d -> mark.equals(d.cartonMark()) && item.getSkuCode().equals(d.skuCode())).mapToLong(ReturnInventoryRequests.SourceDeduction::quantity).sum();
|
||||
if (selected < 0 || selected > value(item.getQuantity())) throw conflict("来源箱 SKU 扣除数量超过库存");
|
||||
if (selected > 0) { items.put(item.getSkuCode(), selected); quantities.merge(item.getSkuCode(), selected, Math::addExact); }
|
||||
}
|
||||
limits.put(mark, items);
|
||||
}
|
||||
if(request.sourceDeductions() != null && request.sourceDeductions().stream().anyMatch(d -> !limits.containsKey(d.cartonMark()) || !limits.get(d.cartonMark()).containsKey(d.skuCode()))) throw badRequest("扣除明细不属于已扫描来源箱");
|
||||
String targetMark = text(request.targetCartonMark());
|
||||
if(targetMark == null && (request.length()==null || request.width()==null || request.height()==null || request.weight()==null)) throw badRequest("新增目标箱必须填写箱规");
|
||||
var planItems=quantities.entrySet().stream().map(e -> new ReturnInventoryRequests.PlanItem(e.getKey(),e.getValue())).toList();
|
||||
String newMark=targetMark==null?packingMarks(List.of(new ReturnInventoryRequests.PlanBox(planItems,request.length(),request.width(),request.height(),request.weight()))).get(0):null;
|
||||
var result = mergeScoped(identity, new ReturnInventoryRequests.Merge(new ArrayList<>(returnNos), quantities.entrySet().stream()
|
||||
.map(e -> new ReturnInventoryRequests.ValidItem(e.getKey(), e.getValue())).toList(), request.length(), request.width(), request.height(), request.weight(), request.idempotencyKey()), limits, targetMark, newMark);
|
||||
ReturnInventoryOperation operation = operations.lockKey(identity.operatorKey(), request.idempotencyKey());
|
||||
operation.setRequestHash(digest); operations.updateById(operation);
|
||||
return result;
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.MergeResult mergeScoped(PdaWarehouseIdentity identity, ReturnInventoryRequests.Merge request,
|
||||
Map<String, Map<String, Long>> limits) {
|
||||
return mergeScoped(identity, request, limits, null);
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.MergeResult mergeScoped(PdaWarehouseIdentity identity, ReturnInventoryRequests.Merge request,
|
||||
Map<String, Map<String, Long>> limits, String targetMark) {
|
||||
return mergeScoped(identity, request, limits, targetMark, null);
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.MergeResult mergeScoped(PdaWarehouseIdentity identity, ReturnInventoryRequests.Merge request,
|
||||
Map<String, Map<String, Long>> limits, String targetMark, String generatedMark) {
|
||||
String requestHash = targetMark == null ? hash(request) : hash(List.of(request, targetMark, limits.keySet()));
|
||||
String key = text(request.idempotencyKey());
|
||||
ReturnInventoryOperation replay = operations.findKey(identity.operatorKey(), key);
|
||||
if (replay != null) return replayMerge(replay, requestHash);
|
||||
@@ -294,20 +858,41 @@ public class ReturnInventoryService {
|
||||
String customerId = selectedOrders.get(0).getCustomerId();
|
||||
if (selectedOrders.stream().anyMatch(order -> !"COMPLETED".equals(order.getStatus()))) throw conflict("只能合并已完成签收的退货单");
|
||||
if (selectedOrders.stream().anyMatch(order -> !customerId.equals(order.getCustomerId()))) throw conflict("不同客户的退货单禁止合箱");
|
||||
ReturnInventoryCarton existingTarget = targetMark == null ? null : cartons.lockMark(targetMark);
|
||||
if (targetMark != null && (existingTarget == null || !customerId.equals(existingTarget.getCustomerId())
|
||||
|| !String.valueOf(identity.overseasWarehouseId()).equals(existingTarget.getOverseasWarehouseId())
|
||||
|| !AVAILABLE.equals(existingTarget.getStatus()) || "PARCEL".equals(existingTarget.getSourceUnitType())
|
||||
|| limits.containsKey(targetMark))) throw conflict("目标箱不存在、被占用、跨客户或与来源箱相同");
|
||||
if (existingTarget != null && cartonItems.lockCarton(existingTarget.getId()).stream().anyMatch(i -> value(i.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("目标箱已有合箱预占,不能并入");
|
||||
if (existingTarget != null) for (var item : cartonItems.lockCarton(existingTarget.getId())) {
|
||||
long tracked = packingTrace.ownership(customerId, targetMark, item.getSkuCode()).stream()
|
||||
.mapToLong(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership::quantity).sum();
|
||||
if (tracked < value(item.getQuantity())) {
|
||||
String[] origins = required(existingTarget.getSourceReturnNo(), "目标箱来源退货单缺失").split(",");
|
||||
if (origins.length != 1) throw conflict("历史目标混箱缺少 SKU 来源明细");
|
||||
packingTrace.insert(IdWorker.getId(), customerId, operation.getId(), "", targetMark,
|
||||
origins[0], item.getSkuCode(), value(item.getQuantity()) - tracked, now);
|
||||
}
|
||||
}
|
||||
List<ReturnInventoryCarton> sources = new ArrayList<>();
|
||||
Map<Long, ReturnOrder> sourceOwners = new LinkedHashMap<>();
|
||||
Map<String, ReturnOrder> candidateOwners = new java.util.TreeMap<>();
|
||||
for (ReturnOrder order : selectedOrders) for (ReturnReceipt receipt : receiptMapper.listOrder(order.getId())) {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
if (carton != null && AVAILABLE.equals(carton.getStatus())) {
|
||||
candidateOwners.put(carton.getCartonMark(), order);
|
||||
}
|
||||
for (ReturnInventoryCarton carton : packingSourceCartons(selectedOrders)) {
|
||||
if (!AVAILABLE.equals(carton.getStatus()) || (limits != null && !limits.containsKey(carton.getCartonMark()))
|
||||
|| (limits == null && !"PARCEL".equals(carton.getSourceUnitType()))) continue;
|
||||
if (!sourceMatches(carton, returnNos)) continue;
|
||||
ReturnOrder owner = selectedOrders.stream().filter(order -> java.util.Arrays.asList(
|
||||
carton.getSourceReturnNo().split(",")).contains(order.getReturnNo())).findFirst().orElse(null);
|
||||
if (owner != null) candidateOwners.put(carton.getCartonMark(), owner);
|
||||
}
|
||||
// All requests acquire source locks in the same order. The earlier
|
||||
// candidate query is only a snapshot: availability must be checked again.
|
||||
for (Map.Entry<String, ReturnOrder> candidate : candidateOwners.entrySet()) {
|
||||
ReturnInventoryCarton locked = cartons.lockMark(candidate.getKey());
|
||||
if (locked != null && AVAILABLE.equals(locked.getStatus())) {
|
||||
if (locked != null && AVAILABLE.equals(locked.getStatus()) && customerId.equals(locked.getCustomerId())
|
||||
&& String.valueOf(identity.overseasWarehouseId()).equals(locked.getOverseasWarehouseId())
|
||||
&& sourceMatches(locked, returnNos) && (limits != null || "PARCEL".equals(locked.getSourceUnitType()))) {
|
||||
sources.add(locked); sourceOwners.put(locked.getId(), candidate.getValue());
|
||||
}
|
||||
}
|
||||
@@ -323,6 +908,7 @@ public class ReturnInventoryService {
|
||||
LinkedHashSet<String> consumedBatchNos = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> consumedReturnNos = new LinkedHashSet<>();
|
||||
Map<Long, Map<String, Long>> attachedByOrder = new LinkedHashMap<>();
|
||||
List<TraceTransfer> transfers = new ArrayList<>();
|
||||
long total = 0;
|
||||
for (ReturnInventoryCarton source : sources) {
|
||||
// Read committed quantities under the source lock, never a stale MVCC snapshot.
|
||||
@@ -330,10 +916,20 @@ public class ReturnInventoryService {
|
||||
long sourceTotal = 0; int sourceTypes = 0;
|
||||
for (ReturnInventoryCartonItem item : sourceItems) {
|
||||
String sku = item.getSkuCode().toUpperCase(java.util.Locale.ROOT);
|
||||
long take = Math.min(value(remaining.get(sku)), value(item.getQuantity()));
|
||||
long free = Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity()));
|
||||
long allowed = limits == null ? free : Math.min(free, value(limits.getOrDefault(source.getCartonMark(), Map.of()).get(sku)));
|
||||
long take = Math.min(value(remaining.get(sku)), allowed);
|
||||
if (take > 0) {
|
||||
if (limits != null) limits.get(source.getCartonMark()).put(sku, allowed - take);
|
||||
consumedBatchNos.add(required(source.getSourceBatchNo(), "来源签收批次缺失,无法合箱"));
|
||||
consumedReturnNos.add(required(source.getSourceReturnNo(), "来源退货单缺失,无法合箱"));
|
||||
consumedReturnNos.addAll(java.util.Arrays.asList(required(source.getSourceReturnNo(), "来源退货单缺失,无法合箱").split(",")));
|
||||
long toTrace = take;
|
||||
for (var origin : ownership(source, sku, value(item.getQuantity()))) {
|
||||
long traced = Math.min(toTrace, origin.quantity());
|
||||
if (traced > 0) transfers.add(new TraceTransfer(source.getCartonMark(), origin.returnNo(), sku, traced));
|
||||
toTrace -= traced;
|
||||
}
|
||||
if (toTrace != 0) throw conflict("来源箱 SKU 的退货单追溯数量不足");
|
||||
remaining.put(sku, remaining.get(sku) - take); total = Math.addExact(total, take);
|
||||
ReturnOrder owner = sourceOwners.get(source.getId());
|
||||
if (owner != null) attachedByOrder.computeIfAbsent(owner.getId(), ignored -> new LinkedHashMap<>())
|
||||
@@ -352,67 +948,77 @@ public class ReturnInventoryService {
|
||||
}
|
||||
if (remaining.values().stream().anyMatch(value -> value > 0)) throw conflict("合箱数量超过对应退货单的可用库存");
|
||||
if (total <= 0) throw badRequest("至少填写一个大于 0 的 SKU 合箱数量");
|
||||
long targetId = IdWorker.getId();
|
||||
ReturnInventoryCarton target = new ReturnInventoryCarton();
|
||||
// Stable across transaction rollback/retry; a generated target ID would leak a new resource on every retry.
|
||||
String palletBusinessKey = "RETURN-MERGE:" + hash(List.of(identity.operatorKey(), key, requestHash));
|
||||
var pallet = physicalPallets.ensureRelabelPallet(identity.overseasWarehouseId(), palletBusinessKey);
|
||||
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId()
|
||||
|| pallet.palletId() <= 0 || text(pallet.palletCode()) == null) {
|
||||
throw conflict("合箱目标托盘资源无效,请核对后重试");
|
||||
}
|
||||
target.setPalletCode(pallet.palletCode());
|
||||
long targetId = existingTarget == null ? IdWorker.getId() : existingTarget.getId();
|
||||
ReturnInventoryCarton target = existingTarget == null ? new ReturnInventoryCarton() : existingTarget;
|
||||
// A merged carton may remain loose; pallet binding is an explicit warehouse operation.
|
||||
target.setSourceUnitType("BOX");
|
||||
target.setId(targetId); target.setCustomerId(customerId); target.setOverseasWarehouseId(sources.get(0).getOverseasWarehouseId());
|
||||
target.setCartonMark(numbers.next("RETURN_CARTON", target.getOverseasWarehouseId(), targetId)); target.setSourceBatchNo(String.join(",", consumedBatchNos)); target.setSourceReturnNo(String.join(",", consumedReturnNos)); target.setStatus(AVAILABLE);
|
||||
target.setSkuTypes(requested.size()); target.setTotalQuantity(total); target.setLength(request.length()); target.setWidth(request.width()); target.setHeight(request.height()); target.setWeight(request.weight()); target.setCreatedAt(now); target.setUpdatedAt(now); cartons.insert(target);
|
||||
for (Map.Entry<String, Long> entry : requested.entrySet()) {
|
||||
ReturnInventoryCartonItem targetItem = new ReturnInventoryCartonItem();
|
||||
targetItem.setId(IdWorker.getId()); targetItem.setCartonId(targetId); targetItem.setCustomerId(customerId); targetItem.setSkuCode(entry.getKey());
|
||||
targetItem.setProductNameZh(productNames.get(entry.getKey())); targetItem.setQuantity(entry.getValue()); cartonItems.insert(targetItem);
|
||||
// A SKU's carton count is the number of available physical cartons containing it:
|
||||
// remove fully emptied source cartons, then add the one merged target carton.
|
||||
long cartonDelta = 1L - emptiedBySku.getOrDefault(entry.getKey(), 0);
|
||||
if (balances.adjustCartonQuantity(customerId, target.getOverseasWarehouseId(), entry.getKey(), cartonDelta) != 1) {
|
||||
throw new IllegalStateException("退货合箱库存箱数更新失败: " + entry.getKey());
|
||||
}
|
||||
if (existingTarget == null) target.setCartonMark(generatedMark == null ? numbers.next("RETURN_CARTON", target.getOverseasWarehouseId(), targetId) : generatedMark);
|
||||
if (existingTarget != null) {
|
||||
if (text(target.getSourceBatchNo()) != null) consumedBatchNos.addAll(java.util.Arrays.asList(target.getSourceBatchNo().split(",")));
|
||||
if (text(target.getSourceReturnNo()) != null) consumedReturnNos.addAll(java.util.Arrays.asList(target.getSourceReturnNo().split(",")));
|
||||
}
|
||||
attachMergedCarton(target, attachedByOrder, selectedOrders, productNames, now);
|
||||
operation.setResultCartonId(targetId); operation.setUpdatedAt(now); operations.updateById(operation);
|
||||
return new ReturnInventoryResponses.MergeResult(target.getCartonMark(), requested.keySet().iterator().next(), total, label(target));
|
||||
target.setSourceBatchNo(String.join(",", consumedBatchNos)); target.setSourceReturnNo(String.join(",", consumedReturnNos)); target.setStatus(AVAILABLE);
|
||||
if (existingTarget == null) {
|
||||
target.setSkuTypes(requested.size()); target.setTotalQuantity(total); target.setLength(request.length()); target.setWidth(request.width());
|
||||
target.setHeight(request.height()); target.setWeight(request.weight()); target.setCreatedAt(now); target.setUpdatedAt(now); cartons.insert(target);
|
||||
}
|
||||
for (Map.Entry<String, Long> entry : requested.entrySet()) {
|
||||
ReturnInventoryCartonItem targetItem = existingTarget == null ? null : cartonItems.lockSku(targetId, entry.getKey());
|
||||
boolean addedSku = targetItem == null;
|
||||
if (targetItem == null) {
|
||||
targetItem = new ReturnInventoryCartonItem(); targetItem.setId(IdWorker.getId()); targetItem.setCartonId(targetId);
|
||||
targetItem.setCustomerId(customerId); targetItem.setSkuCode(entry.getKey()); targetItem.setProductNameZh(productNames.get(entry.getKey()));
|
||||
targetItem.setQuantity(entry.getValue()); targetItem.setPackingReservedQuantity(0L); cartonItems.insert(targetItem);
|
||||
} else { targetItem.setQuantity(Math.addExact(value(targetItem.getQuantity()), entry.getValue())); cartonItems.updateById(targetItem); }
|
||||
long cartonDelta = (addedSku ? 1L : 0L) - emptiedBySku.getOrDefault(entry.getKey(), 0);
|
||||
if (balances.adjustCartonQuantity(customerId, target.getOverseasWarehouseId(), entry.getKey(), cartonDelta) != 1)
|
||||
throw new IllegalStateException("退货合箱库存箱数更新失败: " + entry.getKey());
|
||||
}
|
||||
if (existingTarget != null) {
|
||||
target.setTotalQuantity(cartonItems.totalQuantity(targetId)); target.setSkuTypes(cartonItems.skuTypes(targetId));
|
||||
target.setUpdatedAt(now); target.setLocationCode(null); target.setPutawayAt(null); cartons.updateById(target); cartons.clearPutaway(targetId);
|
||||
}
|
||||
for (TraceTransfer transfer : transfers) packingTrace.insert(IdWorker.getId(), customerId, operation.getId(),
|
||||
transfer.source(), target.getCartonMark(), transfer.returnNo(), transfer.sku(), transfer.quantity(), now);
|
||||
var result = new ReturnInventoryResponses.MergeResult(target.getCartonMark(), requested.keySet().iterator().next(), value(target.getTotalQuantity()), label(target));
|
||||
operation.setResultCartonId(targetId); operation.setResultJson(json(result)); operation.setUpdatedAt(now); operations.updateById(operation);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void attachMergedCarton(ReturnInventoryCarton target, Map<Long, Map<String, Long>> attachedByOrder,
|
||||
List<ReturnOrder> selectedOrders, Map<String, String> productNames, LocalDateTime now) {
|
||||
Map<Long, ReturnOrder> byId = selectedOrders.stream().collect(java.util.stream.Collectors.toMap(ReturnOrder::getId, value -> value));
|
||||
for (Map.Entry<Long, Map<String, Long>> orderEntry : attachedByOrder.entrySet()) {
|
||||
ReturnOrder returnOrder = byId.get(orderEntry.getKey());
|
||||
if (returnOrder == null || returnOrder.getLastlegOrderId() == null) continue;
|
||||
LastlegOrder tail = lastlegOrders.selectById(returnOrder.getLastlegOrderId());
|
||||
if (tail == null || !returnOrder.getCustomerId().equals(tail.getCustomerId())) continue;
|
||||
long quantity = orderEntry.getValue().values().stream().mapToLong(Long::longValue).sum();
|
||||
boolean firstSku = true;
|
||||
for (Map.Entry<String, Long> skuEntry : orderEntry.getValue().entrySet()) {
|
||||
LastlegOrderDetail detail = new LastlegOrderDetail();
|
||||
detail.setId(IdWorker.getId()); detail.setCustomerId(returnOrder.getCustomerId()); detail.setOrderId(tail.getId());
|
||||
detail.setSourceInboundDetailId("RETURN_CARTON:" + target.getId() + ":" + returnOrder.getId() + ":" + skuEntry.getKey());
|
||||
detail.setSkuCode(skuEntry.getKey()); detail.setProductNameZh(productNames.get(skuEntry.getKey()));
|
||||
detail.setPackageCount(firstSku ? 1 : 0); detail.setProductQuantity(Math.toIntExact(skuEntry.getValue())); detail.setShippingMark(target.getCartonMark());
|
||||
detail.setDestinationWarehouseCode(target.getDestinationWarehouseCode()); detail.setDestinationWarehouseName(target.getDestinationWarehouseName());
|
||||
lastlegOrderDetails.insert(detail); firstSku = false;
|
||||
}
|
||||
tail.setPackageCount(Math.addExact(tail.getPackageCount() == null ? 0 : tail.getPackageCount(), 1));
|
||||
tail.setProductQuantity(Math.addExact(tail.getProductQuantity() == null ? 0 : tail.getProductQuantity(), Math.toIntExact(quantity)));
|
||||
tail.setUpdatedAt(now); lastlegOrders.updateById(tail);
|
||||
private record TraceTransfer(String source, String returnNo, String sku, long quantity) { }
|
||||
|
||||
/** Source numbers on a merged carton are a set of original return orders. */
|
||||
public boolean matchesReturnSource(ReturnInventoryCarton carton, String returnNo) {
|
||||
if (carton == null || text(returnNo) == null || text(carton.getSourceReturnNo()) == null) return false;
|
||||
var actual = java.util.Arrays.stream(carton.getSourceReturnNo().split(",")).map(String::trim)
|
||||
.filter(value -> !value.isEmpty()).collect(java.util.stream.Collectors.toSet());
|
||||
var requested = java.util.Arrays.stream(returnNo.split(",")).map(String::trim)
|
||||
.filter(value -> !value.isEmpty()).collect(java.util.stream.Collectors.toSet());
|
||||
return !requested.isEmpty() && actual.containsAll(requested);
|
||||
}
|
||||
|
||||
private List<com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership> ownership(
|
||||
ReturnInventoryCarton carton, String sku, long quantity) {
|
||||
var result = new ArrayList<>(packingTrace.ownership(carton.getCustomerId(), carton.getCartonMark(), sku));
|
||||
long traced = result.stream().mapToLong(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership::quantity).sum();
|
||||
if (traced < quantity) {
|
||||
String[] origins = required(carton.getSourceReturnNo(), "缺少来源退货单").split(",");
|
||||
if (origins.length != 1) throw conflict("历史混箱缺少逐 SKU 来源明细,不能继续合并");
|
||||
result.add(new com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership(origins[0], quantity - traced));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.MergeResult replayMerge(ReturnInventoryOperation operation, String requestHash) {
|
||||
if (!requestHash.equals(operation.getRequestHash())) throw conflict("幂等键已用于不同的合箱请求");
|
||||
if (text(operation.getResultJson()) != null) return read(operation.getResultJson(), new TypeReference<ReturnInventoryResponses.MergeResult>() { });
|
||||
if (operation.getResultCartonId() == null) throw conflict("合箱请求正在处理");
|
||||
// The result and its label must also bypass the snapshot established before the insert race.
|
||||
ReturnInventoryCarton carton = cartons.lockId(operation.getResultCartonId());
|
||||
if (carton == null) throw new IllegalStateException("合箱结果箱不存在");
|
||||
List<ReturnInventoryCartonItem> items = cartonItems.lockCarton(carton.getId());
|
||||
if (items.isEmpty()) throw conflict("历史合箱结果已被后续操作消耗,无法重放旧标签");
|
||||
return new ReturnInventoryResponses.MergeResult(carton.getCartonMark(), items.get(0).getSkuCode(), carton.getTotalQuantity(), label(carton, items));
|
||||
}
|
||||
|
||||
@@ -422,7 +1028,8 @@ public class ReturnInventoryService {
|
||||
|
||||
private ReturnInventoryResponses.Label label(ReturnInventoryCarton carton, List<ReturnInventoryCartonItem> cartonLines) {
|
||||
List<ReturnInventoryResponses.CartonItem> items = cartonLines.stream()
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList();
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity(),
|
||||
Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity())))).toList();
|
||||
return new ReturnInventoryResponses.Label(carton.getCartonMark(), carton.getOverseasWarehouseId(), carton.getCustomerId(),
|
||||
carton.getSourceBatchNo(), carton.getSourceReturnNo(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), items, LocalDateTime.now(), printableHtml(carton, items));
|
||||
}
|
||||
@@ -440,24 +1047,35 @@ public class ReturnInventoryService {
|
||||
|
||||
private ReturnInventoryResponses.CartonDetail cartonDetail(ReturnInventoryCarton carton) {
|
||||
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity())).toList();
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity(),
|
||||
"AVAILABLE".equals(carton.getStatus())
|
||||
? Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity())) : 0)).toList();
|
||||
return new ReturnInventoryResponses.CartonDetail(carton.getCartonMark(), carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getSourceBatchNo(), carton.getSourceReturnNo(),
|
||||
carton.getStatus(), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), carton.getRelabelStatus(), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), items);
|
||||
carton.getStatus(), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), carton.getRelabelStatus(), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), items, carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt(), packingTrace.listCartonHistory(carton.getCustomerId(), carton.getCartonMark()));
|
||||
}
|
||||
|
||||
private static String printableHtml(ReturnInventoryCarton carton, List<ReturnInventoryResponses.CartonItem> items) {
|
||||
StringBuilder rows = new StringBuilder();
|
||||
for (ReturnInventoryResponses.CartonItem item : items) rows.append("<tr><td>").append(escape(item.skuCode())).append("</td><td>")
|
||||
.append(escape(item.productNameZh())).append("</td><td class=\"qty\">").append(item.quantity()).append("</td></tr>");
|
||||
for (ReturnInventoryResponses.CartonItem item : items) {
|
||||
rows.append("<div class=\"line\"><span class=\"sku\">")
|
||||
.append(escape(item.skuCode()))
|
||||
.append("</span></div>");
|
||||
}
|
||||
return "<!doctype html><html><head><meta charset=\"utf-8\"><title>退货箱唛</title><style>"
|
||||
+ "@page{size:100mm 150mm;margin:5mm}body{font-family:Arial,'Microsoft YaHei',sans-serif;color:#111;margin:0}.label{box-sizing:border-box;width:90mm;border:2px solid #111;padding:3mm;min-height:138mm}.brand{font-size:12px;font-weight:700}.title{font-size:16px;font-weight:700;margin:3mm 0}.mark{font:700 16px monospace;word-break:break-all}.meta{font-size:11px;margin:3mm 0;border-top:1px solid #111;border-bottom:1px solid #111;padding:2mm 0;overflow-wrap:anywhere}.barcode{margin:3mm 0}table{width:100%;border-collapse:collapse;font-size:11px;table-layout:fixed}th,td{border:1px solid #111;padding:1mm;text-align:left;overflow-wrap:anywhere}tr{break-inside:avoid}.qty{text-align:right;font-weight:700}.total{font-size:14px;font-weight:700;text-align:right;margin-top:3mm}</style></head><body><div class=\"label\"><div class=\"brand\">BAIYUE OVERSEAS WAREHOUSE</div><div class=\"title\">RETURN CARTON LABEL / 退货箱唛</div><div class=\"mark\">"
|
||||
+ escape(carton.getCartonMark()) + "</div><div class=\"barcode\">" + ReturnCartonBarcode.svg(carton.getCartonMark()) + "</div>"
|
||||
+ "<div class=\"meta\">来源退货单:" + escape(carton.getSourceReturnNo())
|
||||
+ "<br>箱规:" + dimension(carton.getLength()) + " × " + dimension(carton.getWidth()) + " × " + dimension(carton.getHeight()) + " cm / " + dimension(carton.getWeight()) + " kg"
|
||||
+ "<br>签收批次:" + escape(carton.getSourceBatchNo()) + "</div><table><thead><tr><th>SKU</th><th>品名</th><th>数量</th></tr></thead><tbody>"
|
||||
+ rows + "</tbody></table><div class=\"total\">合计:" + carton.getTotalQuantity() + " 件 / " + carton.getSkuTypes() + " 种 SKU</div></div></body></html>";
|
||||
+ "@page{size:100mm 100mm;margin:8mm}*{box-sizing:border-box}body{font-family:Arial,'Microsoft YaHei',sans-serif;color:#111;margin:0}.label{width:84mm;height:84mm;padding:4mm;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;page-break-after:always}.label:last-child{page-break-after:auto}.mark{font:700 14px Arial,sans-serif;line-height:1.25;overflow-wrap:anywhere;margin:0 0 5mm}.barcode{width:100%;max-width:76mm;margin:0 0 4mm}.barcode svg{display:block;width:100%;height:auto}.line{display:flex;justify-content:center;align-items:baseline;width:100%;padding:1mm 0;font-size:12px}.sku{max-width:76mm;overflow-wrap:anywhere;font-weight:700}</style></head><body><div class=\"label\"><div class=\"mark\">"
|
||||
+ escape(carton.getCartonMark()) + "</div><div class=\"barcode\">"
|
||||
+ ReturnCartonBarcode.svg(carton.getCartonMark()) + "</div>" + rows
|
||||
+ "</div></body></html>";
|
||||
}
|
||||
private static String dimension(BigDecimal value) { return value == null ? "—" : value.stripTrailingZeros().toPlainString(); }
|
||||
private List<String> packingMarks(List<ReturnInventoryRequests.PlanBox> boxes) {
|
||||
List<List<String>> groups = boxes.stream().map(box -> box.items() == null ? List.<String>of() : box.items().stream()
|
||||
.map(ReturnInventoryRequests.PlanItem::skuCode).distinct().sorted().toList()).toList();
|
||||
Map<List<String>, Long> totals = groups.stream().collect(java.util.stream.Collectors.groupingBy(group -> group, LinkedHashMap::new, java.util.stream.Collectors.counting()));
|
||||
Map<List<String>, String> prefixes = new HashMap<>(); Map<List<String>, Long> sequences = new HashMap<>();
|
||||
return groups.stream().map(group -> prefixes.computeIfAbsent(group, ignored -> "RC" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase(java.util.Locale.ROOT))
|
||||
+ "#" + totals.get(group) + "-" + sequences.merge(group, 1L, Long::sum)).toList();
|
||||
}
|
||||
private String hash(Object request) { try { return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(String.valueOf(request).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (Exception exception) { throw new IllegalStateException("无法生成请求哈希", exception); } }
|
||||
@@ -472,4 +1090,5 @@ public class ReturnInventoryService {
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
|
||||
public record ReceiptLine(String skuCode, String productNameZh, long quantity) { }
|
||||
public record ReturnOrderLine(String skuCode, String productNameZh, long quantity, long boxCount) { }
|
||||
}
|
||||
|
||||
+197
-29
@@ -31,6 +31,8 @@ import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -69,6 +71,7 @@ public class ReturnReceiptService {
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private LastlegOrderMapper lastlegOrders;
|
||||
@Resource private LastlegOrderDetailMapper lastlegOrderDetails;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper cartonItems;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@@ -192,6 +195,26 @@ public class ReturnReceiptService {
|
||||
result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
/** Backfill tail-leg cargo for receipts created before tail synchronization was added. */
|
||||
@Transactional
|
||||
public void ensureTailCargo(String customerId, long tailOrderId) {
|
||||
if (lastlegOrderDetails == null) return;
|
||||
long existing = lastlegOrderDetails.selectCount(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, tailOrderId));
|
||||
if (existing > 0) return;
|
||||
ReturnOrder order = orders.selectOne(new LambdaQueryWrapper<ReturnOrder>()
|
||||
.eq(ReturnOrder::getCustomerId, customerId).eq(ReturnOrder::getLastlegOrderId, tailOrderId));
|
||||
if (order == null) return;
|
||||
Map<String, ReturnOrderItem> bySku = orderItems.listOrder(order.getId()).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(ReturnOrderItem::getSkuCode, value -> value, (left, right) -> left, LinkedHashMap::new));
|
||||
for (ReturnReceipt receipt : receipts.listOrder(order.getId())) {
|
||||
List<Allocation> allocations = receiptItems.listReceipt(receipt.getId()).stream()
|
||||
.map(line -> new Allocation(bySku.get(line.getSkuCode()), value(line.getReceivedQuantity())))
|
||||
.filter(item -> item.item() != null).toList();
|
||||
if (!allocations.isEmpty()) syncTailCargo(order, receipt, allocations, receipt.getSignedAt() == null ? LocalDateTime.now() : receipt.getSignedAt());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReturnOrderResponses.Detail detail(String customerId, String returnNo) {
|
||||
ReturnOrder order = orders.findNo(customerId, text(returnNo));
|
||||
@@ -199,26 +222,74 @@ public class ReturnReceiptService {
|
||||
List<ReturnOrderItem> orderLines = orderItems.listOrder(order.getId());
|
||||
List<ReturnOrderResponses.Item> items = orderLines.stream().map(this::item).toList();
|
||||
List<ReturnReceipt> orderReceipts = receipts.listOrder(order.getId());
|
||||
List<ReturnOrderResponses.Receipt> receiptViews = orderReceipts.stream().map(receipt ->
|
||||
new ReturnOrderResponses.Receipt(receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getPalletCode(), receipt.getTrackingNo(),
|
||||
receipt.getBoundBy(), value(receipt.getReceivedQuantity()), receipt.getOperatorName(), receipt.getSignedAt())).toList();
|
||||
Map<String, Map<String, ReturnOrderResponses.PalletItem>> palletItems = new LinkedHashMap<>();
|
||||
Map<String, String> productNames = new LinkedHashMap<>();
|
||||
for (ReturnOrderItem line : orderLines) productNames.put(line.getSkuCode(), line.getProductNameZh());
|
||||
List<ReturnOrderResponses.Receipt> receiptViews = orderReceipts.stream().map(receipt -> {
|
||||
List<ReturnOrderResponses.ReceiptItem> lines = receiptItems.listReceipt(receipt.getId()).stream()
|
||||
.map(line -> new ReturnOrderResponses.ReceiptItem(line.getSkuCode(), productNames.get(line.getSkuCode()), value(line.getReceivedQuantity()), value(line.getBoxCount())))
|
||||
.toList();
|
||||
long boxes = value(receipt.getTotalBoxCount());
|
||||
if (boxes == 0) boxes = lines.stream().mapToLong(ReturnOrderResponses.ReceiptItem::boxCount).sum();
|
||||
String cartonMark = text(receipt.getCartonMark());
|
||||
List<ReturnInventoryService.ReturnOrderLine> labelLines = lines.stream()
|
||||
.map(line -> new ReturnInventoryService.ReturnOrderLine(line.skuCode(), line.productNameZh(), line.quantity(), line.boxCount()))
|
||||
.toList();
|
||||
List<ReturnOrderResponses.CartonLabel> labels = inventory.receiptLabels(order, receipt, labelLines);
|
||||
String printableHtml = combineLabels(labels);
|
||||
return new ReturnOrderResponses.Receipt(receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getPalletCode(), receipt.getTrackingNo(),
|
||||
receipt.getBoundBy(), value(receipt.getReceivedQuantity()), receipt.getOperatorName(), receipt.getSignedAt(), receipt.getUnitType(), receipt.getUnitCode(), boxes, lines, cartonMark, printableHtml, labels,
|
||||
inventory.receiptRepairRequired(receipt, boxes));
|
||||
}).toList();
|
||||
Map<String, Map<String, ReturnOrderResponses.PalletItem>> palletItems = new LinkedHashMap<>();
|
||||
Map<String, String> palletTypes = new LinkedHashMap<>();
|
||||
Map<String, Long> physicalBoxes = new LinkedHashMap<>();
|
||||
for (ReturnReceipt receipt : orderReceipts) {
|
||||
if (text(receipt.getPalletCode()) == null) continue;
|
||||
Map<String, ReturnOrderResponses.PalletItem> bySku = palletItems.computeIfAbsent(receipt.getPalletCode(), ignored -> new LinkedHashMap<>());
|
||||
if (!"PALLET".equals(receipt.getUnitType()) && !"BOX".equals(receipt.getUnitType())) continue;
|
||||
String physicalCode = text(receipt.getUnitCode()) == null ? text(receipt.getPalletCode()) : text(receipt.getUnitCode());
|
||||
if (physicalCode == null) continue;
|
||||
String key = receipt.getUnitType() + ":" + physicalCode;
|
||||
palletTypes.put(key, receipt.getUnitType());
|
||||
physicalBoxes.merge(key, value(receipt.getTotalBoxCount()), Long::sum);
|
||||
Map<String, ReturnOrderResponses.PalletItem> bySku = palletItems.computeIfAbsent(key, ignored -> new LinkedHashMap<>());
|
||||
for (ReturnReceiptItem line : receiptItems.listReceipt(receipt.getId())) bySku.merge(line.getSkuCode(),
|
||||
new ReturnOrderResponses.PalletItem(line.getSkuCode(), productNames.get(line.getSkuCode()), value(line.getReceivedQuantity())),
|
||||
(left, right) -> new ReturnOrderResponses.PalletItem(left.skuCode(), text(left.productNameZh()) == null ? right.productNameZh() : left.productNameZh(), left.quantity() + right.quantity()));
|
||||
new ReturnOrderResponses.PalletItem(line.getSkuCode(), productNames.get(line.getSkuCode()), value(line.getReceivedQuantity()), value(line.getBoxCount())),
|
||||
(left, right) -> new ReturnOrderResponses.PalletItem(left.skuCode(), text(left.productNameZh()) == null ? right.productNameZh() : left.productNameZh(), left.quantity() + right.quantity(), left.boxCount() + right.boxCount()));
|
||||
}
|
||||
List<ReturnOrderResponses.PalletDetail> palletViews = palletItems.entrySet().stream().map(entry -> {
|
||||
List<ReturnOrderResponses.PalletItem> palletLines = List.copyOf(entry.getValue().values());
|
||||
return new ReturnOrderResponses.PalletDetail(entry.getKey(), palletLines.size(), palletLines.stream().mapToLong(ReturnOrderResponses.PalletItem::quantity).sum(), palletLines);
|
||||
String[] parts = entry.getKey().split(":", 2);
|
||||
return new ReturnOrderResponses.PalletDetail(parts.length > 1 ? parts[1] : entry.getKey(), palletLines.size(), palletLines.stream().mapToLong(ReturnOrderResponses.PalletItem::quantity).sum(), palletLines,
|
||||
physicalBoxes.getOrDefault(entry.getKey(), 0L), palletTypes.get(entry.getKey()), parts.length > 1 ? parts[1] : entry.getKey());
|
||||
}).toList();
|
||||
List<ReturnOrderResponses.Photo> photos = photoList(order).stream()
|
||||
.map(file -> new ReturnOrderResponses.Photo(file.fileId(), file.fileName(), file.contentType(), file.sizeBytes())).toList();
|
||||
return new ReturnOrderResponses.Detail(summary(order, relabelSubOrders(List.of(order)).getOrDefault(order.getLastlegOrderId(), List.of())), order.getRemark(), items, receiptViews, palletViews, photos);
|
||||
return new ReturnOrderResponses.Detail(summary(order, relabelSubOrders(List.of(order)).getOrDefault(order.getLastlegOrderId(), List.of())), order.getRemark(), items, receiptViews, palletViews, photos,
|
||||
inventory.packingHistory(order.getCustomerId(), order.getReturnNo()));
|
||||
}
|
||||
|
||||
private String combineLabels(List<ReturnOrderResponses.CartonLabel> labels) {
|
||||
if (labels.isEmpty()) return null;
|
||||
// Keep the label's physical page size and barcode layout in the combined print file.
|
||||
java.util.Set<String> styles = new java.util.LinkedHashSet<>();
|
||||
var stylePattern = java.util.regex.Pattern.compile("<style[^>]*>(.*?)</style>", java.util.regex.Pattern.DOTALL | java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
for (var label : labels) {
|
||||
if (label.printableHtml() == null) continue;
|
||||
var matcher = stylePattern.matcher(label.printableHtml());
|
||||
while (matcher.find()) styles.add(matcher.group(1));
|
||||
}
|
||||
StringBuilder html = new StringBuilder("<!doctype html><html><head><meta charset='UTF-8'><style>");
|
||||
styles.forEach(html::append);
|
||||
html.append(".return-label{break-after:page;page-break-after:always}.return-label .label{break-after:auto;page-break-after:auto}.return-label:last-child{break-after:auto;page-break-after:auto}</style></head><body>");
|
||||
for (ReturnOrderResponses.CartonLabel label : labels) {
|
||||
String value = label.printableHtml();
|
||||
if (value == null || value.isBlank()) continue;
|
||||
int bodyStart = value.indexOf("<body");
|
||||
int contentStart = bodyStart < 0 ? 0 : value.indexOf('>', bodyStart) + 1;
|
||||
int bodyEnd = value.lastIndexOf("</body>");
|
||||
String content = contentStart > 0 && bodyEnd > contentStart ? value.substring(contentStart, bodyEnd) : value;
|
||||
html.append("<section class='return-label'>").append(content).append("</section>");
|
||||
}
|
||||
return html.append("</body></html>").toString();
|
||||
}
|
||||
|
||||
public com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway.DownloadedFile downloadPhoto(String customerId, String returnNo, long fileId) {
|
||||
@@ -255,11 +326,12 @@ public class ReturnReceiptService {
|
||||
if (!COMPLETED.equals(order.getStatus())) throw conflict("退货单尚未完成签收");
|
||||
List<ReturnOrderResponses.RelabelCandidate> result = new ArrayList<>();
|
||||
for (ReturnReceipt receipt : receipts.listOrder(order.getId())) {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
if (carton == null || !ReturnInventoryService.AVAILABLE.equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) continue;
|
||||
List<ReturnOrderResponses.Item> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(i -> new ReturnOrderResponses.Item(i.getSkuCode(), i.getProductNameZh(), value(i.getQuantity()), value(i.getQuantity()))).toList();
|
||||
result.add(new ReturnOrderResponses.RelabelCandidate(carton.getId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getStatus(), value(carton.getTotalQuantity()), items, carton.getRelabelStatus()));
|
||||
for (ReturnInventoryCarton carton : cartons.listReceipt(receipt.getId())) {
|
||||
if (!ReturnInventoryService.AVAILABLE.equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) continue;
|
||||
List<ReturnOrderResponses.Item> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(i -> new ReturnOrderResponses.Item(i.getSkuCode(), i.getProductNameZh(), value(i.getQuantity()), value(i.getQuantity()))).toList();
|
||||
result.add(new ReturnOrderResponses.RelabelCandidate(carton.getId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getStatus(), value(carton.getTotalQuantity()), items, carton.getRelabelStatus()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -345,6 +417,8 @@ public class ReturnReceiptService {
|
||||
ReturnInventoryCarton carton = selectedCartons.computeIfAbsent(candidate.getId(), ignored -> cartons.lockId(candidate.getId()));
|
||||
if (carton == null || !"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null)
|
||||
throw conflict("退货箱已被其他换标申请占用,请刷新后重试");
|
||||
if (cartonItems.lockCarton(carton.getId()).stream().anyMatch(line -> value(line.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("退货 SKU 已被合箱计划锁定,请先完成或取消合箱计划");
|
||||
long allocated = Math.min(remaining, skuQuantity);
|
||||
RelabelItem item = new RelabelItem(); item.setId(IdWorker.getId()); item.setApplicationId(appId); item.setCustomerId(customerId);
|
||||
item.setSkuCode(source.getSkuCode());
|
||||
@@ -374,10 +448,10 @@ public class ReturnReceiptService {
|
||||
List<PdaReturnReceiptResponses.CompletedPallet> completedPallets = receipts.listOrder(order.getId()).stream()
|
||||
.filter(receipt -> text(receipt.getPalletCode()) != null)
|
||||
.collect(java.util.stream.Collectors.toMap(ReturnReceipt::getPalletCode, receipt -> {
|
||||
ReturnInventoryCarton carton = cartons.findReceipt(receipt.getId());
|
||||
ReturnInventoryCartonItem item = carton == null ? null : cartonItems.listCarton(carton.getId()).stream().findFirst().orElse(null);
|
||||
return new PdaReturnReceiptResponses.CompletedPallet(receipt.getPalletCode(), item == null ? "" : item.getSkuCode(),
|
||||
item == null ? 0 : value(item.getQuantity()), receipt.getReceiptBatchNo());
|
||||
List<ReturnReceiptItem> lines = receiptItems.listReceipt(receipt.getId());
|
||||
String sku = lines.size() == 1 ? lines.get(0).getSkuCode() : "MIXED";
|
||||
return new PdaReturnReceiptResponses.CompletedPallet(receipt.getPalletCode(), sku,
|
||||
lines.stream().mapToLong(line -> value(line.getReceivedQuantity())).sum(), receipt.getReceiptBatchNo());
|
||||
}, (left, right) -> new PdaReturnReceiptResponses.CompletedPallet(left.palletCode(), left.skuCode(),
|
||||
left.quantity() + right.quantity(), right.receiptBatchNo()), java.util.LinkedHashMap::new))
|
||||
.values().stream().toList();
|
||||
@@ -432,6 +506,9 @@ public class ReturnReceiptService {
|
||||
String startedBy = norm(request.startedBy());
|
||||
if (!Set.of("RETURN_NO", "SKU").contains(startedBy)) throw badRequest("startedBy 必须为 RETURN_NO 或 SKU");
|
||||
String trackingNo = text(request.trackingNo());
|
||||
String unitType = norm(request.unitType());
|
||||
if (unitType == null) unitType = "PALLET";
|
||||
String unitCode = text(request.unitCode());
|
||||
ReturnOrder order = orders.lockNo(text(request.returnNo()));
|
||||
if (order == null || !String.valueOf(identity.overseasWarehouseId()).equals(order.getOverseasWarehouseId())) {
|
||||
throw notFound("当前绑定仓没有该退货单");
|
||||
@@ -448,8 +525,34 @@ public class ReturnReceiptService {
|
||||
}
|
||||
List<ReturnOrderItem> lockedItems = orderItems.lockOrder(order.getId());
|
||||
Map<String, Long> receivedBySku = aggregate(request.items());
|
||||
Map<String, Long> boxCountBySku = request.items().stream().filter(item -> item.boxCount() != null)
|
||||
.collect(java.util.stream.Collectors.toMap(item -> text(item.skuCode()), PdaReturnReceiptRequests.Item::boxCount, Math::addExact, LinkedHashMap::new));
|
||||
List<Allocation> allocations = allocate(lockedItems, receivedBySku);
|
||||
long receiptQuantity = allocations.stream().mapToLong(Allocation::quantity).sum();
|
||||
if (!Set.of("PALLET", "BOX", "PARCEL").contains(unitType)) throw badRequest("unitType 必须为 PALLET、BOX 或 PARCEL");
|
||||
for (PdaReturnReceiptRequests.Item input : request.items()) {
|
||||
if (!"PARCEL".equals(unitType) && (input.boxCount() == null || input.boxCount() <= 0)) throw badRequest("托盘或箱子签收的每 SKU 箱数必须大于 0");
|
||||
if ("PARCEL".equals(unitType) && input.boxCount() != null && input.boxCount() != 0) throw badRequest("包裹签收不填写箱数");
|
||||
}
|
||||
// 托盘总箱数以本次各 SKU 箱数之和为准,避免操作员重复填写并产生不一致。
|
||||
long totalBoxCount = "BOX".equals(unitType) ? 1 : "PALLET".equals(unitType)
|
||||
? boxCountBySku.values().stream().mapToLong(Long::longValue).sum() : 0;
|
||||
if ("PALLET".equals(unitType) && totalBoxCount <= 0) throw badRequest("托盘签收的总箱数必须大于 0");
|
||||
if ("PALLET".equals(unitType) && boxCountBySku.values().stream().anyMatch(count -> count > totalBoxCount))
|
||||
throw badRequest("每 SKU 箱数不能超过本托总箱数");
|
||||
if ("BOX".equals(unitType) && boxCountBySku.values().stream().anyMatch(count -> count != 1))
|
||||
throw badRequest("单箱签收时每 SKU 箱数应为 1");
|
||||
if ("PARCEL".equals(unitType) && request.totalBoxCount() != null) throw badRequest("包裹签收不填写箱数");
|
||||
if (unitCode == null && "PALLET".equals(unitType)) unitCode = text(request.palletCode());
|
||||
if (unitCode == null && "BOX".equals(unitType))
|
||||
unitCode = "RC" + hash(List.of(order.getReturnNo(), IdWorker.getId())).substring(0, 16).toUpperCase(java.util.Locale.ROOT) + "#1-1";
|
||||
if (unitCode == null) throw badRequest(("PALLET".equals(unitType) ? "托盘码" : "包裹码") + "不能为空");
|
||||
if ("PALLET".equals(unitType)
|
||||
&& (request.length() == null || request.width() == null || request.height() == null || request.weight() == null
|
||||
|| request.length().signum() <= 0 || request.width().signum() <= 0
|
||||
|| request.height().signum() <= 0 || request.weight().signum() <= 0)) {
|
||||
throw badRequest("托盘签收必须填写有效的长、宽、高和重量");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long receiptId = IdWorker.getId();
|
||||
ReturnReceipt receipt = new ReturnReceipt();
|
||||
@@ -457,7 +560,10 @@ public class ReturnReceiptService {
|
||||
receipt.setReceiptNo(numbers.next("RETURN_RECEIPT", order.getOverseasWarehouseId(), receiptId));
|
||||
receipt.setReceiptBatchNo(numbers.next("RETURN_BATCH", order.getOverseasWarehouseId(), receiptId));
|
||||
receipt.setTrackingNo(trackingNo); receipt.setBoundBy(startedBy); receipt.setStatus("SIGNED");
|
||||
receipt.setPalletCode(text(request.palletCode()));
|
||||
receipt.setUnitType(unitType); receipt.setUnitCode(unitCode);
|
||||
receipt.setTotalBoxCount(totalBoxCount);
|
||||
receipt.setPalletCode("PALLET".equals(unitType) ? unitCode : ("BOX".equals(unitType) ? text(request.palletCode()) : null));
|
||||
receipt.setLength(request.length()); receipt.setWidth(request.width()); receipt.setHeight(request.height()); receipt.setWeight(request.weight());
|
||||
receipt.setReceivedQuantity(receiptQuantity); receipt.setOperatorId(identity.operatorKey());
|
||||
receipt.setOperatorName(identity.operatorName()); receipt.setIdempotencyKey(key); receipt.setRequestHash(requestHash);
|
||||
receipt.setSignedAt(now); receipt.setCreatedAt(now);
|
||||
@@ -474,7 +580,7 @@ public class ReturnReceiptService {
|
||||
ReturnReceiptItem receiptItem = new ReturnReceiptItem();
|
||||
receiptItem.setId(IdWorker.getId()); receiptItem.setReceiptId(receiptId); receiptItem.setReturnOrderItemId(item.getId());
|
||||
receiptItem.setCustomerId(order.getCustomerId()); receiptItem.setSkuCode(item.getSkuCode());
|
||||
receiptItem.setReceivedQuantity(allocation.quantity()); receiptItems.insert(receiptItem);
|
||||
receiptItem.setReceivedQuantity(allocation.quantity()); receiptItem.setBoxCount("PARCEL".equals(unitType) ? null : boxCountBySku.get(item.getSkuCode())); receiptItems.insert(receiptItem);
|
||||
}
|
||||
Map<String, ReturnInventoryService.ReceiptLine> inventoryLines = new LinkedHashMap<>();
|
||||
for (Allocation allocation : allocations) {
|
||||
@@ -484,18 +590,22 @@ public class ReturnReceiptService {
|
||||
left.skuCode(), left.productNameZh() == null ? right.productNameZh() : left.productNameZh(),
|
||||
Math.addExact(left.quantity(), right.quantity())));
|
||||
}
|
||||
if (allocations.stream().map(allocation -> allocation.item().getSkuCode()).distinct().count() != 1) {
|
||||
throw conflict("一个签收批次只能签收一个 SKU");
|
||||
List<ReturnInventoryCarton> inventoryCartons;
|
||||
if ("PALLET".equals(unitType)) {
|
||||
inventoryCartons = inventory.createReceiptCartons(order, receipt, new ArrayList<>(inventoryLines.values()), boxCountBySku);
|
||||
} else if ("PARCEL".equals(unitType)) {
|
||||
inventoryCartons = new ArrayList<>();
|
||||
for (var line : inventoryLines.values())
|
||||
inventoryCartons.add(inventory.createReceiptCarton(order, receipt, null, List.of(line)));
|
||||
} else {
|
||||
inventoryCartons = List.of(inventory.createReceiptCarton(order, receipt, unitCode, new ArrayList<>(inventoryLines.values())));
|
||||
}
|
||||
receipt.setCartonMark(inventory.createReceiptCarton(order, receipt, text(request.palletCode()),
|
||||
new ArrayList<>(inventoryLines.values())).getCartonMark());
|
||||
receipt.setCartonMark(inventoryCartons.get(0).getCartonMark());
|
||||
receipts.updateById(receipt);
|
||||
syncTailCargo(order, receipt, allocations, now);
|
||||
List<ReturnOrderItem> finalItems = orderItems.listOrder(order.getId());
|
||||
long received = finalItems.stream().mapToLong(item -> value(item.getReceivedQuantity())).sum();
|
||||
boolean completed = finalItems.stream().allMatch(item -> value(item.getReceivedQuantity()) == value(item.getExpectedQuantity()));
|
||||
if (completed && photoFiles != null && photoList(order).isEmpty()) {
|
||||
throw conflict("本次将完成签收,请先拍摄并上传退货打托照片");
|
||||
}
|
||||
order.setReceivedQuantity(received); order.setStatus(completed ? COMPLETED : PARTIALLY_RECEIVED); order.setUpdatedAt(now); orders.updateById(order);
|
||||
syncReturnTail(order, completed ? "INBOUND_COMPLETED" : "PARTIALLY_RECEIVED", completed || received > 0);
|
||||
ReturnReceiptBatch batch = new ReturnReceiptBatch();
|
||||
@@ -517,6 +627,50 @@ public class ReturnReceiptService {
|
||||
lastlegOrders.updateById(tail);
|
||||
}
|
||||
|
||||
/** Keep the platform tail-leg order detail in sync with PDA receipt facts. */
|
||||
private void syncTailCargo(ReturnOrder order, ReturnReceipt receipt, List<Allocation> allocations, LocalDateTime now) {
|
||||
if (lastlegOrderDetails == null || order.getLastlegOrderId() == null) return;
|
||||
LastlegOrder tail = lastlegOrders.selectById(order.getLastlegOrderId());
|
||||
if (tail == null || !Objects.equals(order.getCustomerId(), tail.getCustomerId())) return;
|
||||
Map<String, Long> quantities = new LinkedHashMap<>();
|
||||
Map<String, Long> boxes = new LinkedHashMap<>();
|
||||
Map<String, String> names = new LinkedHashMap<>();
|
||||
for (Allocation allocation : allocations) {
|
||||
String sku = allocation.item().getSkuCode();
|
||||
quantities.merge(sku, allocation.quantity(), Math::addExact);
|
||||
names.putIfAbsent(sku, allocation.item().getProductNameZh());
|
||||
}
|
||||
for (ReturnReceiptItem line : receiptItems.listReceipt(receipt.getId())) {
|
||||
boxes.merge(line.getSkuCode(), value(line.getBoxCount()), Math::addExact);
|
||||
}
|
||||
int packageCount = 0;
|
||||
boolean firstSku = true;
|
||||
for (Map.Entry<String, Long> entry : quantities.entrySet()) {
|
||||
int skuBoxes = Math.toIntExact("PARCEL".equals(receipt.getUnitType()) ? 1 : boxes.getOrDefault(entry.getKey(), 0L));
|
||||
if (skuBoxes <= 0) skuBoxes = 1;
|
||||
LastlegOrderDetail detail = new LastlegOrderDetail();
|
||||
detail.setId(IdWorker.getId()); detail.setCustomerId(order.getCustomerId()); detail.setOrderId(tail.getId());
|
||||
detail.setSourceInboundDetailId("RETURN_RECEIPT:" + receipt.getId() + ":" + entry.getKey());
|
||||
detail.setSkuCode(entry.getKey()); detail.setProductNameZh(names.get(entry.getKey())); detail.setPackageCount(skuBoxes);
|
||||
detail.setProductQuantity(Math.toIntExact(entry.getValue())); detail.setPalletCount("PALLET".equals(receipt.getUnitType()) ? 1 : 0);
|
||||
detail.setGrossWeightKg(firstSku ? receipt.getWeight() : null); detail.setVolumeCbm(firstSku && receipt.getLength() != null && receipt.getWidth() != null && receipt.getHeight() != null
|
||||
? receipt.getLength().multiply(receipt.getWidth()).multiply(receipt.getHeight()).divide(java.math.BigDecimal.valueOf(1_000_000)) : null);
|
||||
detail.setShippingMark(receipt.getCartonMark());
|
||||
lastlegOrderDetails.insert(detail);
|
||||
packageCount = Math.addExact(packageCount, skuBoxes);
|
||||
firstSku = false;
|
||||
}
|
||||
tail.setPackageCount(Math.addExact(tail.getPackageCount() == null ? 0 : tail.getPackageCount(), packageCount));
|
||||
tail.setReceivedPackageCount(Math.addExact(tail.getReceivedPackageCount() == null ? 0 : tail.getReceivedPackageCount(), packageCount));
|
||||
tail.setProductQuantity(Math.addExact(tail.getProductQuantity() == null ? 0 : tail.getProductQuantity(), Math.toIntExact(allocations.stream().mapToLong(Allocation::quantity).sum())));
|
||||
if ("PALLET".equals(receipt.getUnitType())) tail.setPalletCount(Math.addExact(tail.getPalletCount() == null ? 0 : tail.getPalletCount(), 1));
|
||||
if (receipt.getLength() != null && receipt.getWidth() != null && receipt.getHeight() != null) {
|
||||
tail.setVolumeCbm(receipt.getLength().multiply(receipt.getWidth()).multiply(receipt.getHeight()).divide(java.math.BigDecimal.valueOf(1_000_000)));
|
||||
}
|
||||
tail.setActualArrivalAt(tail.getActualArrivalAt() == null ? now : tail.getActualArrivalAt());
|
||||
tail.setUpdatedAt(now); lastlegOrders.updateById(tail);
|
||||
}
|
||||
|
||||
private List<RelabelPhotoFileGateway.UploadedFile> photoList(ReturnOrder order) {
|
||||
return photoFiles == null ? List.of() : photoFiles.list(order.getCustomerId(), order.getId());
|
||||
}
|
||||
@@ -558,8 +712,22 @@ public class ReturnReceiptService {
|
||||
return confirmed(order, receipt, COMPLETED.equals(order.getStatus()));
|
||||
}
|
||||
private PdaReturnReceiptResponses.Confirmed confirmed(ReturnOrder order, ReturnReceipt receipt, boolean completed) {
|
||||
String cartonMark = text(receipt.getCartonMark());
|
||||
String printableHtml = null;
|
||||
List<ReturnOrderResponses.CartonLabel> labels = new java.util.ArrayList<>();
|
||||
if (receipt.getId() != null) {
|
||||
for (ReturnInventoryCarton carton : cartons.listReceipt(receipt.getId())) {
|
||||
var label = inventory.label(order.getCustomerId(), carton.getCartonMark());
|
||||
if (label != null) labels.add(new ReturnOrderResponses.CartonLabel(carton.getCartonMark(), label.printableHtml()));
|
||||
}
|
||||
}
|
||||
if (cartonMark != null) {
|
||||
var label = inventory.label(order.getCustomerId(), cartonMark);
|
||||
if (label != null) printableHtml = label.printableHtml();
|
||||
}
|
||||
return new PdaReturnReceiptResponses.Confirmed(order.getReturnNo(), receipt.getReceiptNo(), receipt.getReceiptBatchNo(), receipt.getPalletCode(),
|
||||
order.getStatus(), completed, value(order.getExpectedQuantity()), value(order.getReceivedQuantity()), receipt.getSignedAt());
|
||||
order.getStatus(), completed, value(order.getExpectedQuantity()), value(order.getReceivedQuantity()), receipt.getSignedAt(), receipt.getUnitType(), receipt.getUnitCode(),
|
||||
cartonMark, printableHtml, value(receipt.getTotalBoxCount()), labels);
|
||||
}
|
||||
private Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabelSubOrders(List<ReturnOrder> values) {
|
||||
var ids = values.stream().map(ReturnOrder::getLastlegOrderId).filter(java.util.Objects::nonNull).distinct().toList();
|
||||
|
||||
@@ -23,6 +23,7 @@ public final class OverseasPermissions {
|
||||
public static final String INVENTORY_EXPORT = "platform:overseas-warehouse:inventory:export";
|
||||
public static final String INVENTORY_EDIT = "platform:overseas-warehouse:inventory:edit";
|
||||
public static final String INVENTORY_ADJUST = "platform:overseas-warehouse:inventory:adjust";
|
||||
public static final String INVENTORY_MANAGE = "lastleg:inventory:manage";
|
||||
public static final String DELIVERY_LIST = "platform:overseas-warehouse:delivery:list";
|
||||
public static final String DELIVERY_RETURN = "platform:overseas-warehouse:delivery:return";
|
||||
public static final String DELIVERY_STATUS = "platform:overseas-warehouse:delivery:status";
|
||||
@@ -79,6 +80,7 @@ public final class OverseasPermissions {
|
||||
public static final String LOOSEBOARD_CREATE = "platform:overseas-business:looseboard:create";
|
||||
public static final String LOOSEBOARD_EDIT = "platform:overseas-business:looseboard:edit";
|
||||
public static final String LOOSEBOARD_STATUS = "platform:overseas-business:looseboard:status";
|
||||
public static final String CARGO_DISCARD_LIST = "platform:overseas-warehouse:discard:list";
|
||||
public static final String QUOTE_LIST = "platform:overseas-master-data:quote:list";
|
||||
public static final String QUOTE_CREATE = "platform:overseas-master-data:quote:create";
|
||||
public static final String QUOTE_EDIT = "platform:overseas-master-data:quote:edit";
|
||||
|
||||
@@ -576,6 +576,7 @@ public class LastlegOrderService {
|
||||
String currentStatus = StringUtils.hasText(query.currentStatus()) ? query.currentStatus() : query.status();
|
||||
String warehouse = StringUtils.hasText(query.warehouse()) ? query.warehouse().trim() : null;
|
||||
return new LambdaQueryWrapper<LastlegOrder>()
|
||||
.ne(LastlegOrder::getOrderType, "CARGO_DISCARD")
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.and(StringUtils.hasText(query.orderNo()), nested -> nested
|
||||
.like(LastlegOrder::getOrderNo, query.orderNo())
|
||||
|
||||
Reference in New Issue
Block a user