贴标申请,仓储转运

This commit is contained in:
2026-10-10 16:58:33 +08:00
parent 05302490fb
commit c6471d06ff
6 changed files with 105 additions and 13 deletions
@@ -82,7 +82,12 @@ public final class DeliveryRequests {
@Size(max=128) String skuCode,
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType,
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
@jakarta.validation.constraints.Positive Long quantity) {
@jakarta.validation.constraints.Positive Long quantity,
@Size(min=1, max=200) List<@NotNull @Valid ReturnSkuOutboundItem> items) {
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
List<String> cartonMarks, String remark, String idempotencyKey, String skuCode, String unitType, Long quantity) {
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, unitType, quantity, null);
}
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
List<String> cartonMarks, String remark, String idempotencyKey, String skuCode, String unitType) {
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, unitType, null);
@@ -96,6 +101,10 @@ public final class DeliveryRequests {
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, null);
}
}
public record ReturnSkuOutboundItem(@NotBlank @Size(max=128) String cartonMark,
@NotBlank @Size(max=128) String skuCode,
@NotNull @jakarta.validation.constraints.Positive
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class) Long quantity) { }
public record WarehouseTransferOutbound(
@NotBlank @Size(max=64) String transferNo,
@NotNull @Size(min=1, max=100) List<@NotNull Long> palletIds,
@@ -499,10 +499,23 @@ public class DeliveryTransferService {
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
String warehouseId = request.overseasWarehouseId().trim();
boolean itemMode = request.items() != null;
boolean skuMode = itemMode || StringUtils.hasText(request.skuCode());
Map<String, DeliveryRequests.ReturnSkuOutboundItem> requestedItems = new LinkedHashMap<>();
if (itemMode) {
if (request.items().isEmpty() || StringUtils.hasText(request.skuCode()) || request.quantity() != null || "PALLET".equals(request.unitType()))
throw conflict("散件明细出库不可同时提交统一SKU、数量或整托方式");
for (var line : request.items()) {
if (line == null || !StringUtils.hasText(line.cartonMark()) || !StringUtils.hasText(line.skuCode()) || line.quantity() == null || line.quantity() <= 0
|| requestedItems.putIfAbsent(line.cartonMark().trim(), line) != null) throw conflict("散件出库明细不能为空、数量必须大于0且来源不可重复");
}
}
if (request.quantity() != null && (!StringUtils.hasText(request.skuCode()) || request.quantity() <= 0
|| "PALLET".equals(request.unitType()))) throw conflict("按件数出库必须选择单SKU,不能同时整托出库");
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(StringUtils::hasText).distinct().toList();
if (marks.size() != request.cartonMarks().size()) throw conflict("箱唛不能为空且不可重复");
if (itemMode && (marks.size() != requestedItems.size() || !requestedItems.keySet().containsAll(marks)))
throw conflict("散件出库明细与所选来源不一致");
LocalDateTime now = LocalDateTime.now();
List<ReturnInventoryCarton> locked = new ArrayList<>();
for (String mark : marks.stream().sorted().toList()) {
@@ -516,23 +529,27 @@ public class DeliveryTransferService {
existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
for (ReturnInventoryCarton carton : locked) {
if (StringUtils.hasText(request.skuCode()) && !"PARCEL".equals(carton.getSourceUnitType()))
if (skuMode && !"PARCEL".equals(carton.getSourceUnitType()))
throw conflict("单SKU出库只允许散件库存;箱内库存请按箱或托盘出库");
if ("PARCEL".equals(carton.getSourceUnitType()) && !StringUtils.hasText(request.skuCode()))
if ("PARCEL".equals(carton.getSourceUnitType()) && !skuMode)
throw conflict("包裹散件只能按 SKU 出库,不能按箱号或托盘出库");
com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountGuard.requireConfirmed(carton);
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())))
String selectedSku = itemMode ? requestedItems.get(carton.getCartonMark()).skuCode().trim() : trim(request.skuCode());
if (skuMode && (contents.size() != 1
|| !selectedSku.equals(contents.get(0).getSkuCode())))
throw conflict("所选散件来源必须仅包含指定SKU");
if (itemMode && requestedItems.get(carton.getCartonMark()).quantity() > value(contents.get(0).getQuantity()))
throw conflict("出库件数超过所选散件来源可用数量:" + carton.getCartonMark());
if ("PALLET".equals(request.unitType()) && !StringUtils.hasText(carton.getPalletCode()))
throw conflict("整托出库必须存在托盘绑定");
}
Map<String, Long> skuQuantities = new LinkedHashMap<>();
if (StringUtils.hasText(request.skuCode())) {
if (itemMode) requestedItems.forEach((mark, line) -> skuQuantities.put(mark, line.quantity()));
else if (StringUtils.hasText(request.skuCode())) {
long available = locked.stream().mapToLong(c -> value(c.getTotalQuantity())).sum();
long remaining = request.quantity() == null ? available : request.quantity();
if (remaining > available) throw conflict("出库件数超过所选来源库存数量");
@@ -564,7 +581,7 @@ public class DeliveryTransferService {
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(1); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo());
// Packed stock is still picked by its physical carton mark; loose parcels use SKU scanning.
item.setSkuCode("PARCEL".equals(carton.getSourceUnitType()) ? trim(request.skuCode()) : null);
item.setSkuCode("PARCEL".equals(carton.getSourceUnitType()) ? itemMode ? requestedItems.get(carton.getCartonMark()).skuCode().trim() : trim(request.skuCode()) : null);
item.setReturnSkuQuantity(skuQuantities.get(carton.getCartonMark()));
item.setPalletNo("PALLET".equals(request.unitType()) ? carton.getPalletCode() : null);
item.setCreatedAt(now); outboundPallets.insert(item);