贴标申请,仓储转运

This commit is contained in:
2026-10-12 11:18:58 +08:00
parent 29b562ebf5
commit b4b00ae3dd
39 changed files with 990 additions and 315 deletions
@@ -486,7 +486,6 @@ public class DeliveryTransferService {
public List<DeliveryResponses.ReturnOutboundCandidate> returnOutboundCandidates(String customerId, String warehouseId) {
if (!StringUtils.hasText(warehouseId)) return List.of();
return returnCartons.findAvailable(required(customerId, "Customer is required"), warehouseId.trim()).stream()
.filter(carton -> !"PARCEL".equals(carton.getSourceUnitType()))
.map(c -> new DeliveryResponses.ReturnOutboundCandidate(c.getSourceReturnNo(), c.getOverseasWarehouseId(),
c.getCartonMark(), c.getPalletCode(), c.getFbaCartonMark(), c.getDestinationWarehouseCode(), c.getDestinationWarehouseName(),
value(c.getTotalQuantity()), c.getStatus())).toList();
@@ -499,23 +498,16 @@ 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,不能同时整托出库");
// A return outbound is always a physical package/carton operation. SKU and
// quantity are selection fields for the packing workflow, never a direct
// outbound command. Partial package/carton quantities must be materialized
// into a new labelled carton before they can be picked.
boolean itemMode = request.items() != null && !request.items().isEmpty();
boolean skuMode = itemMode || StringUtils.hasText(request.skuCode()) || request.quantity() != null;
if (skuMode)
throw conflict("退货出库最小单位为包裹/箱;部分数量请先合箱并打印新箱标签后再出库");
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()) {
@@ -529,37 +521,18 @@ public class DeliveryTransferService {
existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
for (ReturnInventoryCarton carton : locked) {
if (skuMode && !"PARCEL".equals(carton.getSourceUnitType()))
throw conflict("单SKU出库只允许散件库存;箱内库存请按箱或托盘出库");
if ("PARCEL".equals(carton.getSourceUnitType()) && !skuMode)
throw conflict("包裹散件只能按 SKU 出库,不能按箱号或托盘出库");
if ("PACKING_RESERVED".equals(carton.getStatus()))
throw conflict("该包裹/箱正在合箱,请先完成或取消合箱");
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());
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 (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("出库件数超过所选来源库存数量");
for (ReturnInventoryCarton carton : locked) {
long selected = Math.min(remaining, value(carton.getTotalQuantity()));
if (selected > 0) skuQuantities.put(carton.getCartonMark(), selected);
remaining -= selected;
}
locked = locked.stream().filter(c -> skuQuantities.containsKey(c.getCartonMark())).toList();
}
// No quantity allocation is performed here. The selected physical units
// are shipped whole; partials arrive only as newly-created merge cartons.
if ("PALLET".equals(request.unitType())) {
String pallet = locked.get(0).getPalletCode();
if (locked.stream().anyMatch(c -> !Objects.equals(pallet, c.getPalletCode()))) throw conflict("整托出库只能选择同一托盘");
@@ -580,9 +553,8 @@ public class DeliveryTransferService {
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());
// Packed stock is still picked by its physical carton mark; loose parcels use SKU scanning.
item.setSkuCode("PARCEL".equals(carton.getSourceUnitType()) ? itemMode ? requestedItems.get(carton.getCartonMark()).skuCode().trim() : trim(request.skuCode()) : null);
item.setReturnSkuQuantity(skuQuantities.get(carton.getCartonMark()));
// Packages and cartons are picked by their physical barcode.
item.setSkuCode(null); item.setReturnSkuQuantity(null);
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);
@@ -38,7 +38,19 @@ public final class RelabelResponses {
String destinationFbaWarehouseName, String skuCode, String productNameZh,
long availablePackages, long version, List<InventoryPosition> positions,
String sourceType, Long returnCartonId, String cartonMark,
List<SkuItem> skuItems, long relabelledPackages) {}
List<SkuItem> skuItems, long relabelledPackages, String relabelStatus,
long relabelledQuantity, long relabelReservedQuantity) {
public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode,
String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName,
String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh,
long availablePackages, long version, List<InventoryPosition> positions, String sourceType,
Long returnCartonId, String cartonMark, List<SkuItem> skuItems, long relabelledPackages) {
this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId,
overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName,
skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark,
skuItems, relabelledPackages, null, relabelledPackages, 0);
}
}
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
String relabelOrderNo, String orderType, String status, Integer packageCount,
LocalDateTime createdAt) {}
@@ -6,6 +6,25 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RelabelItemMapper extends BaseMapper<RelabelItem> {
@org.apache.ibatis.annotations.Select("SELECT ri.* FROM ll_relabel_item ri JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"WHERE ri.customer_id=#{customerId} AND ri.return_carton_id=#{cartonId} AND ri.application_id<>#{currentApplicationId} " +
"AND ra.status NOT IN ('CANCELLED','REJECTED') AND ra.customer_confirmed_at IS NOT NULL " +
"AND EXISTS(SELECT 1 FROM ll_relabel_pda_unit_fact f WHERE f.item_id=ri.id AND f.result='RELABELLED') " +
"AND NOT EXISTS(SELECT 1 FROM ll_relabel_item newer JOIN ll_relabel_application na ON na.id=newer.application_id " +
"WHERE newer.return_carton_id=ri.return_carton_id AND newer.application_id<>#{currentApplicationId} AND na.status NOT IN ('CANCELLED','REJECTED') " +
"AND (COALESCE(newer.source_carton_mark,newer.carton_mark)=COALESCE(ri.source_carton_mark,ri.carton_mark) " +
"OR COALESCE(newer.source_carton_mark,newer.carton_mark)=(SELECT c.carton_mark FROM ll_return_inventory_carton c WHERE c.id=#{cartonId})) " +
"AND (na.created_at>ra.created_at OR (na.created_at=ra.created_at AND na.id>ra.id))) ORDER BY ri.id")
java.util.List<RelabelItem> priorCompletedReturnItems(@org.apache.ibatis.annotations.Param("customerId") String customerId,
@org.apache.ibatis.annotations.Param("cartonId") long cartonId,
@org.apache.ibatis.annotations.Param("currentApplicationId") long currentApplicationId);
@org.apache.ibatis.annotations.Update("UPDATE ll_relabel_item i JOIN ll_relabel_application a ON a.id=i.application_id " +
"SET i.return_carton_id=#{targetId},i.carton_mark=#{targetMark} WHERE i.customer_id=#{customerId} " +
"AND i.return_carton_id=#{sourceId} AND a.status NOT IN ('CANCELLED','REJECTED') AND a.customer_confirmed_at IS NOT NULL")
int moveCompletedReturnCarton(@org.apache.ibatis.annotations.Param("customerId") String customerId,
@org.apache.ibatis.annotations.Param("sourceId") long sourceId,
@org.apache.ibatis.annotations.Param("targetId") long targetId,
@org.apache.ibatis.annotations.Param("targetMark") String targetMark);
@org.apache.ibatis.annotations.Select("SELECT id FROM ll_inventory_balance WHERE id=#{balanceId} AND customer_id=#{customerId} FOR UPDATE")
Long lockSourceBalance(@org.apache.ibatis.annotations.Param("customerId") String customerId,
@org.apache.ibatis.annotations.Param("balanceId") Long balanceId);
@@ -86,6 +86,13 @@ public class PdaRelabelController {
identity.overseasWarehouseId(), relabelNo, request));
}
@PostMapping("/{relabelNo}/return-cartons/pack")
public ApiResponse<PdaRelabelResponses.Detail> packReturn(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String relabelNo, @Valid @RequestBody com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.BoxMerge request) {
Identity identity = identity(user);
return ApiResponse.success(service.packReturn(identity.operatorKey(), identity.operatorName(), identity.overseasWarehouseId(), relabelNo, request));
}
@PostMapping("/{relabelNo}/return-skus/complete-total")
public ApiResponse<PdaRelabelResponses.Detail> completeReturnSkuTotal(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String relabelNo, @Valid @RequestBody PdaRelabelRequests.BulkSkuTotal request) {
@@ -17,7 +17,13 @@ public final class PdaRelabelRequests {
public record SkuScan(@NotNull @Positive Long itemId, @Min(0) int skuIndex,
@NotBlank @Size(max = 255) String oldSku,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 255) String idempotencyKey) { }
@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 128) String cartonMark,
@Size(max = 128) String newSkuCode, @Positive Integer quantity) {
public SkuScan(Long itemId, int skuIndex, String oldSku, Integer expectedVersion, String idempotencyKey) {
this(itemId, skuIndex, oldSku, expectedVersion, idempotencyKey, null, null, null);
}
}
public record CartonScan(@NotBlank @Size(max = 255) String cartonMark,
@NotNull @Min(0) Integer expectedVersion,
@@ -40,7 +46,13 @@ public final class PdaRelabelRequests {
public record BulkSku(@NotNull @Positive Long itemId,
@Positive int quantity,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 255) String idempotencyKey) { }
@NotBlank @Size(max = 255) String idempotencyKey,
@NotBlank @Size(max = 128) String cartonMark,
@NotBlank @Size(max = 128) String skuCode) {
public BulkSku(Long itemId, int quantity, Integer expectedVersion, String idempotencyKey) {
this(itemId, quantity, expectedVersion, idempotencyKey, null, null);
}
}
public record BulkSkuTotal(@NotBlank @Size(max = 255) String skuCode,
@Positive int quantity,
@@ -20,7 +20,20 @@ public final class PdaRelabelResponses {
String operatorName, String reviewerName, List<Long> completionPhotoFileIds,
String completionRemark, String reviewDecision, String reviewRemark,
LocalDateTime claimedAt, LocalDateTime submittedAt, LocalDateTime reviewedAt,
List<Item> items, List<UnitFact> unitFacts, List<History> history) { }
List<Item> items, List<UnitFact> unitFacts, List<History> history, String deliveryMethod,
boolean requiresPalletBinding) {
public Detail(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline, String contactName,
String contactPhone, String applicationStatus, String operationType, String taskStatus, int expectedPackages,
int relabelledPackages, int skippedPackages, int exceptionPackages, int version, String operatorName,
String reviewerName, List<Long> completionPhotoFileIds, String completionRemark, String reviewDecision,
String reviewRemark, LocalDateTime claimedAt, LocalDateTime submittedAt, LocalDateTime reviewedAt,
List<Item> items, List<UnitFact> unitFacts, List<History> history) {
this(relabelNo, customerOrderNo, labelDeadline, contactName, contactPhone, applicationStatus, operationType,
taskStatus, expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, version,
operatorName, reviewerName, completionPhotoFileIds, completionRemark, reviewDecision, reviewRemark,
claimedAt, submittedAt, reviewedAt, items, unitFacts, history, null, false);
}
}
public record Item(Long itemId, String skuCode, String productNameZh, String shipmentNo,
String trackingCode, String newSkuCode, String labelRequirement,
@@ -29,7 +42,18 @@ public final class PdaRelabelResponses {
List<SkuItem> skuItems,
int expectedPackages, int relabelledPackages, int skippedPackages,
int exceptionPackages, int cartonProcessedPackages, int skuProcessedPackages,
String destinationFbaWarehouseCode) { }
String destinationFbaWarehouseCode, Long returnCartonId, long returnQuantity) {
public Item(Long itemId, String skuCode, String productNameZh, String shipmentNo, String trackingCode,
String newSkuCode, String labelRequirement, String relabelFileId, String relabelFileName, String relabelFileUrl,
String cartonMark, String fbaCartonMark, String operationType, List<SkuItem> skuItems, int expectedPackages,
int relabelledPackages, int skippedPackages, int exceptionPackages, int cartonProcessedPackages,
int skuProcessedPackages, String destinationFbaWarehouseCode) {
this(itemId, skuCode, productNameZh, shipmentNo, trackingCode, newSkuCode, labelRequirement,
relabelFileId, relabelFileName, relabelFileUrl, cartonMark, fbaCartonMark, operationType, skuItems,
expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, cartonProcessedPackages,
skuProcessedPackages, destinationFbaWarehouseCode, null, 0);
}
}
public record SkuItem(String skuCode, String newSkuCode, int quantity, String remark) { }
public record UnitFact(Long factId, Long itemId, String oldLabel, String newLabel,
@@ -52,6 +52,32 @@ import java.util.stream.Collectors;
@Service
public class PdaRelabelService {
@Resource private com.baiyue.lastleg.relabel.service.ReturnRelabelPackingService returnPacking;
@Transactional
public PdaRelabelResponses.Detail packReturn(String operatorKey, String operatorName, long warehouseId,
String relabelNo, com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.BoxMerge request) {
UnitCommand command = new UnitCommand(relabelNo, "RETURN_PACK", request);
var replay = replay(operatorKey, warehouseId, relabelNo, "RETURN_PACK", request.idempotencyKey(), command);
if (replay != null) return replay;
var app = requireApplication(relabelNo, warehouseId, true);
var task = requireOwnedTask(app, operatorKey, true);
var selected = itemList(app).stream().filter(i -> i.getReturnCartonId() != null
&& Set.of("RETURN_BULK_SKU_LABEL", "SKU_LABEL").contains(operationType(i))).toList();
if (selected.isEmpty() || selected.size() != itemList(app).size()) throw conflict("仅退货按件换标任务可使用此合箱操作");
var facts = factList(task);
for (var item : selected) if (quantity(facts.stream().filter(f -> item.getId().equals(f.getItemId())).toList(), "RELABELLED")
!= value(item.getNotifiedPackageCount())) throw conflict("请先完成每个来源包裹/箱的换标数量登记,再合箱");
var sourceDeductions = returnPacking.deductions(selected);
var packed = returnPacking.pack(new com.baiyue.lastleg.pda.security.PdaWarehouseIdentity(operatorKey, operatorName, warehouseId), app, selected, request);
updateTask(task, task.getVersion());
insertHistory(task, "RETURN_PACK", PROCESSING, PROCESSING, operatorKey, operatorName,
Map.of("cartonMark", packed.cartonMark(), "quantity", packed.totalQuantity(), "label", packed.label(),
"round", mixedRound(task), "sourceCartonMarks", request.sourceCartonMarks(), "sourceDeductions", sourceDeductions));
var result = detail(app, task);
saveOperation(task, operatorKey, "RETURN_PACK", request.idempotencyKey(), command, result);
return result;
}
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard bindingGuard;
public static final String AVAILABLE = "AVAILABLE";
public static final String PROCESSING = "PROCESSING";
@@ -230,11 +256,16 @@ public class PdaRelabelService {
var task = requireOwnedTask(app, operatorKey, true);
requireVersion(task, request.expectedVersion());
var item = requireItem(app, request.itemId());
if (item.getReturnCartonId() != null && !Objects.equals(item.getCartonMark(), trim(request.cartonMark())))
throw conflict("请扫描当前来源包裹/箱号后再扫描 SKU");
if (!Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(operationType(item))) throw conflict("该箱没有SKU换标任务");
if (value(item.getNotifiedPackageCount()) != 1) throw conflict("SKU逐条作业要求每个仓库唛头独立一箱");
var skus = readSkuItems(item.getSkuRelabelItemsJson());
if (request.skuIndex() < 0 || request.skuIndex() >= skus.size()) throw conflict("SKU明细不存在");
var sku = skus.get(request.skuIndex());
if (item.getReturnCartonId() != null && (!Objects.equals(sku.newSkuCode(), trim(request.newSkuCode()))
|| !Objects.equals(sku.quantity(), request.quantity())))
throw conflict("新 SKU 和件数必须与已批准的来源箱明细一致");
if (!Objects.equals(sku.skuCode(), request.oldSku().trim()) || !StringUtils.hasText(sku.newSkuCode()) || sku.quantity() <= 0)
throw conflict("旧SKU或绑定的新标签、数量不正确");
if (factList(task).stream().anyMatch(fact -> item.getId().equals(fact.getItemId()))
@@ -275,6 +306,9 @@ public class PdaRelabelService {
requireVersion(task, request.expectedVersion());
RelabelItem item = requireItem(application, request.itemId());
if (!"RETURN_BULK_SKU_LABEL".equals(operationType(item))) throw conflict("当前明细不是退货按件贴标任务");
if (!Objects.equals(item.getCartonMark(), trim(request.cartonMark()))
|| !Objects.toString(item.getSkuCode(), "").equalsIgnoreCase(Objects.toString(trim(request.skuCode()), "")))
throw conflict("请扫描当前来源包裹/箱号和对应 SKU,不能登记其他来源");
int completed = quantity(factList(task).stream().filter(fact -> item.getId().equals(fact.getItemId())).toList(), "RELABELLED");
int remaining = value(item.getNotifiedPackageCount()) - completed;
if (request.quantity() > remaining) throw conflict("本次完成数量超过该 SKU 剩余贴标数量");
@@ -309,6 +343,8 @@ public class PdaRelabelService {
.filter(item -> skuCode.equalsIgnoreCase(Objects.toString(item.getSkuCode(), "").trim()))
.toList();
if (skuItems.isEmpty()) throw notFound("当前换标单没有该退货 SKU");
if (skuItems.stream().map(RelabelItem::getCartonMark).distinct().count() != 1)
throw conflict("同 SKU 来自多个包裹/箱,请逐来源扫码登记,不能自动分配");
List<PdaRelabelUnitFact> existingFacts = factList(task);
Map<Long, Integer> completedByItem = new HashMap<>();
for (RelabelItem item : skuItems) {
@@ -574,6 +610,9 @@ public class PdaRelabelService {
throw conflict("Relabel item already has all required package facts");
}
String normalizedOldLabel = trim(oldLabel);
// A merged return carton can carry several independent SKU task items.
// Product-label facts are unique by item; old_label's global key is for physical carton-label scans.
if (item.getReturnCartonId() != null && "SKU_LABEL".equals(operationType(item))) normalizedOldLabel = null;
if (normalizedOldLabel != null && unitFacts.findOldLabel(normalizedOldLabel) != null) {
throw conflict("Old label was already processed by another relabel unit");
}
@@ -746,7 +785,8 @@ public class PdaRelabelService {
quantity(itemFacts, "EXCEPTION"),
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "CARTON") ? 1 : 0),
count(itemFacts, "RELABELLED") + (itemFacts.isEmpty() && mixedStageDone(task, item.getId(), "SKU") ? 1 : 0),
item.getDestinationFbaWarehouseCode());
item.getDestinationFbaWarehouseCode(), item.getReturnCartonId(),
returnQuantity(item));
}).toList();
List<PdaRelabelResponses.UnitFact> factResponses = facts.stream().map(fact ->
new PdaRelabelResponses.UnitFact(fact.getId(), fact.getItemId(), fact.getOldLabel(), fact.getNewLabel(),
@@ -768,7 +808,7 @@ public class PdaRelabelService {
task == null ? null : task.getCompletionRemark(), task == null ? null : task.getReviewDecision(),
task == null ? null : task.getReviewRemark(), task == null ? null : task.getClaimedAt(),
task == null ? null : task.getSubmittedAt(), task == null ? null : task.getReviewedAt(),
itemResponses, factResponses, historyResponses);
itemResponses, factResponses, historyResponses, application.getDeliveryMethod(), "TRUCK".equals(application.getDeliveryMethod()));
}
private String effectiveTaskStatus(RelabelApplication application, PdaRelabelTask task) {
@@ -841,6 +881,10 @@ public class PdaRelabelService {
List.class, PdaRelabelResponses.SkuItem.class));
} catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read relabel SKU details", exception); }
}
private long returnQuantity(RelabelItem item) {
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
return carton == null || carton.getTotalQuantity() == null ? 0L : carton.getTotalQuantity();
}
private static String operationType(RelabelItem item) {
return StringUtils.hasText(item.getOperationType()) ? item.getOperationType() : "CARTON_LABEL";
}
@@ -264,7 +264,7 @@ public class RelabelApplicationService implements WalletResultHandler {
balance.getProductNameZh(), available, value(balance.getVersion()), positions,
"INBOUND", null, null, List.of(), 0);
}).filter(candidate -> candidate.availablePackages() > 0).toList());
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, operationWarehouseId.trim())) {
for (ReturnInventoryCarton carton : returnCartons.findRelabelCandidates(customerId, operationWarehouseId.trim())) {
String keyword = trim(inboundCode);
if (StringUtils.hasText(keyword)
&& !containsIgnoreCase(carton.getCartonMark(), keyword)
@@ -272,22 +272,22 @@ public class RelabelApplicationService implements WalletResultHandler {
List<RelabelResponses.SkuItem> cartonSkus = returnCartonItems.listCarton(carton.getId()).stream()
.filter(item -> value(item.getQuantity()) > 0)
.map(item -> {
long used = returnCartons.relabelledQuantity(carton.getId(), item.getSkuCode());
long remaining = value(item.getQuantity()) - used - value(item.getPackingReservedQuantity());
long remaining = "AVAILABLE".equals(carton.getStatus())
? value(item.getQuantity()) - value(item.getPackingReservedQuantity()) : 0;
return new RelabelResponses.SkuItem(item.getSkuCode(), null,
Math.toIntExact(Math.max(remaining, 0)), null);
}).filter(item -> item.quantity() > 0).toList();
}).toList();
if (cartonSkus.isEmpty()) continue;
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
.collect(Collectors.joining(","));
long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
if (availablePackages <= 0) continue;
long relabelledPackages = carton.getTotalQuantity() == null ? 0
: Math.max(0, carton.getTotalQuantity() - availablePackages);
long relabelledPackages = returnCartons.totalRelabelledQuantity(carton.getId());
long reserved = returnCartons.relabelReservedQuantity(carton.getId());
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
carton.getDestinationWarehouseName(), skuCodes, "退货入库库存", availablePackages, 0, List.of(),
"PARCEL".equals(carton.getSourceUnitType()) ? "RETURN_PARCEL" : "RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages));
"PARCEL".equals(carton.getSourceUnitType()) ? "RETURN_PARCEL" : "RETURN_CARTON", carton.getId(), carton.getCartonMark(), cartonSkus, relabelledPackages,
com.baiyue.lastleg.returnreceipt.service.ReturnRelabelProgress.status(value(carton.getTotalQuantity()), relabelledPackages, reserved), relabelledPackages, reserved));
}
return result;
}
@@ -370,6 +370,7 @@ public class RelabelApplicationService implements WalletResultHandler {
Set<String> selectedCartonMarks = new HashSet<>();
Map<Long, ReturnInventoryCarton> selectedReturnCartons = new LinkedHashMap<>();
Map<Long, Long> selectedReturnPackages = new LinkedHashMap<>();
Map<Long, Map<String, Long>> selectedReturnSkus = new LinkedHashMap<>();
for (RelabelRequests.Item item : request.items()) {
validateOperation(item);
if (item.notifiedPackageCount() > item.totalPackageCount()) {
@@ -401,9 +402,8 @@ public class RelabelApplicationService implements WalletResultHandler {
final long sourceReturnCartonId = carton.getId();
Map<String, Long> availableSku = returnCartonItems.lockCarton(sourceReturnCartonId).stream()
.collect(Collectors.toMap(line -> line.getSkuCode().toUpperCase(java.util.Locale.ROOT),
line -> Math.max(0, value(line.getQuantity()) - value(line.getPackingReservedQuantity())
- returnCartons.relabelledQuantity(sourceReturnCartonId, line.getSkuCode())), Long::sum));
Map<String, Long> requestedSku = new java.util.HashMap<>();
line -> Math.max(0, value(line.getQuantity()) - value(line.getPackingReservedQuantity())), Long::sum));
Map<String, Long> requestedSku = selectedReturnSkus.computeIfAbsent(carton.getId(), ignored -> new java.util.HashMap<>());
if (item.skuItems() != null) for (var skuItem : item.skuItems()) {
long quantity = requestedSku.merge(skuItem.skuCode().toUpperCase(java.util.Locale.ROOT), (long) skuItem.quantity(), Long::sum);
if (quantity > availableSku.getOrDefault(skuItem.skuCode().toUpperCase(java.util.Locale.ROOT), 0L))
@@ -414,10 +414,9 @@ public class RelabelApplicationService implements WalletResultHandler {
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);
if (requestedPackages + previouslyRelabelled > value(carton.getTotalQuantity())) {
if (requestedPackages > value(carton.getTotalQuantity())) {
throw conflict("退货托盘 " + carton.getCartonMark() + " 可用箱数不足");
}
sources.add(new SourceInventory(item, null, carton));
@@ -495,7 +494,8 @@ public class RelabelApplicationService implements WalletResultHandler {
}
}
for (String returnNo : selectedReturnCartons.values().stream().map(ReturnInventoryCarton::getSourceReturnNo)
.filter(StringUtils::hasText).distinct().sorted().toList()) {
.filter(StringUtils::hasText).flatMap(value -> java.util.Arrays.stream(value.split(",")))
.map(String::trim).filter(StringUtils::hasText).distinct().sorted().toList()) {
ReturnOrder returnOrder = returnOrders.lockCustomerNo(customerId, returnNo);
if (returnOrder != null) {
returnOrder.setRelabelApplicationId(applicationId);
@@ -618,11 +618,15 @@ public class RelabelApplicationService implements WalletResultHandler {
}
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
validateTransition(application.getStatus(), next);
if (Set.of("REJECTED", "CANCELLED").contains(next) && itemList(application.getId()).stream()
.filter(item -> item.getReturnCartonId() != null)
.anyMatch(item -> returnCartons.totalRelabelledQuantity(item.getReturnCartonId()) > 0))
throw conflict("该退货任务已有实际商品换标事实,不能取消并恢复为未换标库存;请按异常完工或退回重做处理");
if (PENDING_REVIEW.equals(application.getStatus()) && PENDING_PROCESSING.equals(next)
&& itemList(application.getId()).stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(operationType(item)))) {
boolean hasCartonFile = StringUtils.hasText(application.getCartonTotalFileId());
boolean hasSkuFile = StringUtils.hasText(application.getSkuTotalFileId());
if (hasCartonFile == hasSkuFile) throw conflict("退货贴标审核前必须且只能上传一个箱唛总文件或 SKU 总文件");
if (!hasCartonFile && !hasSkuFile) throw conflict("退货贴标审核前必须上传箱唛总文件或 SKU 总文件,两类资料可分别上传");
}
if ("PROCESSING".equals(next)
&& !Set.of("FROZEN", "NOT_REQUIRED").contains(application.getWalletStatus())) {
@@ -686,11 +690,10 @@ public class RelabelApplicationService implements WalletResultHandler {
insertOrderHistory(customerId, order.getId(), orderBefore, orderStatus, request.remark(), now);
}
}
ReturnOrder returnOrder = returnOrders.selectOne(new LambdaQueryWrapper<ReturnOrder>()
List<ReturnOrder> linkedReturnOrders = returnOrders.selectList(new LambdaQueryWrapper<ReturnOrder>()
.eq(ReturnOrder::getRelabelApplicationId, application.getId())
.eq(ReturnOrder::getCustomerId, customerId)
.last("LIMIT 1"));
if (returnOrder != null) {
.eq(ReturnOrder::getCustomerId, customerId));
for (ReturnOrder returnOrder : linkedReturnOrders) {
returnOrder.setRelabelStatus(next);
returnOrder.setUpdatedAt(now);
returnOrders.updateById(returnOrder);
@@ -938,11 +941,10 @@ public class RelabelApplicationService implements WalletResultHandler {
request.remark(), now);
}
}
ReturnOrder returnOrder = returnOrders.selectOne(new LambdaQueryWrapper<ReturnOrder>()
List<ReturnOrder> completedReturnOrders = returnOrders.selectList(new LambdaQueryWrapper<ReturnOrder>()
.eq(ReturnOrder::getRelabelApplicationId, application.getId())
.eq(ReturnOrder::getCustomerId, customerId)
.last("LIMIT 1"));
if (returnOrder != null) {
.eq(ReturnOrder::getCustomerId, customerId));
for (ReturnOrder returnOrder : completedReturnOrders) {
returnOrder.setRelabelStatus("COMPLETED");
returnOrder.setUpdatedAt(now);
returnOrders.updateById(returnOrder);
@@ -961,6 +963,7 @@ public class RelabelApplicationService implements WalletResultHandler {
carton.setStatus(StringUtils.hasText(application.getOutboundOrderNo()) ? "OUTBOUND_PENDING"
: awaitingTruck ? "RELABEL_PENDING" : "AVAILABLE");
carton.setUpdatedAt(now); returnCartons.updateById(carton);
if (carton.getRelabelApplicationId() == null) returnCartons.clearRelabel(carton.getId());
}
insertHistory(customerId, application.getId(), before, application.getStatus(), operatorId,
request.remark(), now);
@@ -1295,6 +1298,7 @@ public class RelabelApplicationService implements WalletResultHandler {
&& "RELABEL_PENDING".equals(carton.getStatus())) {
carton.setRelabelApplicationId(null); carton.setRelabelNo(null); carton.setRelabelStatus(null);
carton.setStatus("AVAILABLE"); carton.setUpdatedAt(LocalDateTime.now()); returnCartons.updateById(carton);
returnCartons.clearRelabel(carton.getId());
}
continue;
}
@@ -22,12 +22,13 @@ public class RelabelBindingGuard {
@Resource private PdaRelabelHistoryMapper histories;
@Resource private InventoryPositionMapper positions;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper returnLines;
@Resource private ObjectMapper objectMapper;
public void requireBound(RelabelApplication app) {
List<RelabelItem> cartons = items.selectList(new QueryWrapper<RelabelItem>()
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId()))
.stream().filter(item -> Set.of("CARTON_LABEL", "CARTON_AND_SKU", "RETURN_BULK_SKU_LABEL")
.stream().filter(item -> (item.getReturnCartonId() != null && "SKU_LABEL".equals(item.getOperationType())) || Set.of("CARTON_LABEL", "CARTON_AND_SKU", "RETURN_BULK_SKU_LABEL")
.contains(item.getOperationType())).toList();
if (cartons.isEmpty()) return;
Map<String, Set<String>> bindings = new HashMap<>();
@@ -44,7 +45,7 @@ public class RelabelBindingGuard {
} catch (Exception e) { throw conflict("组托记录无法读取,请核对后重试"); }
}
for (RelabelItem item : cartons) {
if (item.getReturnCartonId() != null && !"RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
if (item.getReturnCartonId() != null && !Set.of("RETURN_BULK_SKU_LABEL", "SKU_LABEL").contains(item.getOperationType())) {
var carton = returnCartons.selectById(item.getReturnCartonId());
if (carton == null || !app.getCustomerId().equals(carton.getCustomerId())) throw conflict("退货箱来源缺失");
if (!"TRUCK".equals(app.getDeliveryMethod())) continue;
@@ -52,7 +53,20 @@ public class RelabelBindingGuard {
throw conflict("退货箱尚未完成排车托盘绑定");
continue;
}
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
if (Set.of("RETURN_BULK_SKU_LABEL", "SKU_LABEL").contains(item.getOperationType()) && item.getReturnCartonId() != null) {
var sameCarton = cartons.stream().filter(i -> Objects.equals(i.getReturnCartonId(), item.getReturnCartonId())).toList();
var stock = item.getReturnCartonId() == null ? List.<com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem>of() : returnLines.lockCarton(item.getReturnCartonId());
var selected = new HashMap<String, Long>();
for (var i : sameCarton) {
if ("RETURN_BULK_SKU_LABEL".equals(i.getOperationType())) selected.merge(i.getSkuCode().toUpperCase(Locale.ROOT), (long)i.getNotifiedPackageCount(), Long::sum);
else try {
for (var sku : objectMapper.readTree(i.getSkuRelabelItemsJson())) selected.merge(sku.path("skuCode").asText().toUpperCase(Locale.ROOT), sku.path("quantity").asLong(), Long::sum);
} catch (Exception failure) { throw conflict("换标明细无法读取"); }
}
var actual = new HashMap<String, Long>();
stock.forEach(i -> actual.merge(i.getSkuCode().toUpperCase(Locale.ROOT), i.getQuantity(), Long::sum));
if (!selected.equals(actual)) throw conflict("部分换标货物必须先合箱生成独立新箱,再提交完工");
if (!"TRUCK".equals(app.getDeliveryMethod())) continue;
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
if (carton == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode())
|| !"PROCESSING".equals(carton.getRelabelStatus()))
@@ -0,0 +1,136 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests;
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.*;
/** Called inside the owned PDA task transaction; materializes only its completed pieces. */
@Service
public class ReturnRelabelPackingService {
@Resource private ReturnInventoryService inventory;
@Resource private ReturnInventoryCartonMapper cartons;
@Resource private RelabelItemMapper items;
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper cartonItems;
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnRelabelCompletionLedgerMapper completionLedger;
@Resource private com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelUnitFactMapper facts;
@Resource private com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper histories;
@Resource private com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper tasks;
@Resource private com.fasterxml.jackson.databind.ObjectMapper json;
@org.springframework.transaction.annotation.Transactional
public ReturnInventoryResponses.MergeResult pack(PdaWarehouseIdentity identity, RelabelApplication app,
List<RelabelItem> selected, ReturnInventoryRequests.BoxMerge request) {
if (request.targetCartonMark() != null || selected.isEmpty()) throw conflict("换标合箱必须生成独立新箱");
var marks = selected.stream().map(RelabelItem::getCartonMark).distinct().sorted().toList();
if (!new HashSet<>(marks).equals(new HashSet<>(request.sourceCartonMarks()))) throw conflict("请扫描本次换标全部来源包裹/箱");
if (selected.stream().map(RelabelItem::getDestinationFbaWarehouseCode).distinct().count() != 1)
throw conflict("不同目的仓不能合箱");
var deductions = deductions(selected);
if (request.sourceDeductions() != null && !totals(deductions).equals(totals(request.sourceDeductions())))
throw conflict("合箱数量必须等于本次已完成换标的明细数量");
var inheritedTransfers = new ArrayList<Map<String, Object>>();
for (String mark : marks) {
var source = cartons.lockMark(mark);
if (source == null || !app.getCustomerId().equals(source.getCustomerId())
|| !app.getId().equals(source.getRelabelApplicationId()) || !"RELABEL_PENDING".equals(source.getStatus()))
throw conflict("来源包裹/箱已变化,请刷新");
var prior = items.priorCompletedReturnItems(app.getCustomerId(), source.getId(), app.getId());
if (!prior.isEmpty()) {
var ids = prior.stream().map(i -> i.getId().toString()).toList();
var raw = inheritedQuantities(prior);
var physical = new HashMap<String, Long>();
cartonItems.lockCarton(source.getId()).forEach(i -> physical.merge(i.getSkuCode().toUpperCase(Locale.ROOT), i.getQuantity(), Math::addExact));
var moved = new ArrayList<Map<String, Object>>();
for (var entry : totals(deductions).entrySet()) {
if (!mark.equals(entry.getKey().get(0))) continue;
String sku = entry.getKey().get(1).toUpperCase(Locale.ROOT);
long inherited = raw.getOrDefault(sku, 0L) - completionLedger.deductedQuantity(ids, sku);
long stock = physical.getOrDefault(sku, 0L);
if (inherited < 0 || inherited > stock) throw conflict("历史换标实物归属与来源库存不一致,请核对来源");
if (inherited > 0 && inherited < stock && entry.getValue() < stock)
throw conflict("已换标与未换标货物来源不能自动猜测,请整箱隔离后重新换标");
long transfer = Math.min(inherited, entry.getValue());
if (transfer > 0) moved.add(Map.of("skuCode", sku, "quantity", transfer));
}
if (!moved.isEmpty()) inheritedTransfers.add(Map.of("sourceItemIds", ids, "sourceCartonMark", mark, "items", moved));
}
source.setStatus("AVAILABLE"); cartons.updateById(source);
}
var result = inventory.mergeBoxes(identity, new ReturnInventoryRequests.BoxMerge(marks, null,
"RELABEL-PACK:" + app.getId() + ":" + request.idempotencyKey(), deductions,
request.length(), request.width(), request.height(), request.weight()));
for (String mark : marks) {
var source = cartons.lockMark(mark);
cartons.clearRelabel(source.getId());
}
var target = cartons.lockMark(result.cartonMark());
target.setStatus("RELABEL_PENDING"); target.setRelabelApplicationId(app.getId());
target.setRelabelNo(app.getRelabelNo()); target.setRelabelStatus("PROCESSING");
target.setDestinationWarehouseCode(selected.get(0).getDestinationFbaWarehouseCode());
cartons.updateById(target);
for (var item : selected) {
item.setReturnCartonId(target.getId()); item.setCartonMark(target.getCartonMark());
items.updateById(item);
}
if (!inheritedTransfers.isEmpty()) {
var task = tasks.find(app.getCustomerId(), app.getId());
for (var transfer : inheritedTransfers) {
var detail = new LinkedHashMap<String, Object>(transfer);
detail.put("targetCartonMark", result.cartonMark()); detail.put("targetApplicationId", app.getId().toString());
var history = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
history.setId(com.baomidou.mybatisplus.core.toolkit.IdWorker.getId()); history.setTaskId(task.getId());
history.setApplicationId(app.getId()); history.setCustomerId(app.getCustomerId());
history.setAction("RETURN_COMPLETION_TRANSFER"); history.setFromStatus("PROCESSING"); history.setToStatus("PROCESSING");
history.setOperatorKey(identity.operatorKey()); history.setOperatorName(identity.operatorName());
try { history.setDetailJson(json.writeValueAsString(detail)); }
catch (java.io.IOException failure) { throw new IllegalStateException(failure); }
history.setCreatedAt(java.time.LocalDateTime.now()); histories.insert(history);
}
}
return result;
}
private Map<String, Long> inheritedQuantities(List<RelabelItem> prior) {
var completed = facts.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact>()
.in("item_id", prior.stream().map(RelabelItem::getId).toList()).eq("result", "RELABELLED"));
var result = new HashMap<String, Long>();
for (var item : prior) {
long count = completed.stream().filter(f -> item.getId().equals(f.getItemId())).mapToLong(f -> f.getQuantity() == null ? 1 : f.getQuantity()).sum();
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) result.merge(item.getSkuCode().toUpperCase(Locale.ROOT), count, Math::addExact);
else for (var deduction : deductions(List.of(item)))
result.merge(deduction.skuCode().toUpperCase(Locale.ROOT), Math.multiplyExact(count, deduction.quantity()), Math::addExact);
}
return result;
}
public List<ReturnInventoryRequests.SourceDeduction> deductions(List<RelabelItem> selected) {
var deductions = new ArrayList<ReturnInventoryRequests.SourceDeduction>();
for (var i : selected) {
if ("RETURN_BULK_SKU_LABEL".equals(i.getOperationType())) deductions.add(new ReturnInventoryRequests.SourceDeduction(i.getCartonMark(), i.getSkuCode(), i.getNotifiedPackageCount()));
else try {
for (var sku : json.readTree(i.getSkuRelabelItemsJson())) deductions.add(new ReturnInventoryRequests.SourceDeduction(i.getCartonMark(), sku.path("skuCode").asText(), sku.path("quantity").asLong()));
} catch (java.io.IOException failure) { throw conflict("换标 SKU 明细无法读取"); }
}
return deductions;
}
private Map<List<String>, Long> totals(List<ReturnInventoryRequests.SourceDeduction> lines) {
var result = new LinkedHashMap<List<String>, Long>();
for (var line : lines) {
if (line.quantity() <= 0) throw conflict("合箱扣减数量必须为正数");
result.merge(List.of(line.cartonMark(), line.skuCode()), line.quantity(), Math::addExact);
}
return result;
}
private RuntimeException conflict(String reason) {
return new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, reason);
}
}
@@ -24,6 +24,11 @@ public class PdaReturnInventoryController {
@Resource private ReturnInventoryService service;
@Resource private com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountService counting;
@PostMapping("/cartons/{cartonMark}/confirm-label")
public ApiResponse<ReturnInventoryResponses.Label> confirmLabel(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String cartonMark) {
return ApiResponse.success(service.confirmLabel(PdaWarehouseIdentity.require(user), cartonMark));
}
@GetMapping("/counting/tasks")
public ApiResponse<com.baiyue.common.core.api.PageResult<com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Task>> countingTasks(
@AuthenticationPrincipal AuthenticatedUser user, @Valid com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Query query) {
@@ -45,9 +45,13 @@ public final class ReturnInventoryRequests {
@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) {
@Size(max = 500) List<@NotBlank String> sourceCartonMarks,
@Size(max = 1000) List<@Valid SourceDeduction> sourceDeductions) {
public PackingPlanCreate(List<String> returnNos, String mode, List<PlanBox> boxes, String remark, String idempotencyKey, List<String> sourceCartonMarks) {
this(returnNos, mode, boxes, remark, idempotencyKey, sourceCartonMarks, null);
}
public PackingPlanCreate(List<String> returnNos, String mode, List<PlanBox> boxes, String remark, String idempotencyKey) {
this(returnNos, mode, boxes, remark, idempotencyKey, List.of());
this(returnNos, mode, boxes, remark, idempotencyKey, List.of(), null);
}
}
@@ -12,7 +12,8 @@ public final class ReturnInventoryResponses {
public record CartonSummary(String cartonMark, String overseasWarehouseId, String sourceBatchNo,
String sourceReturnNo, String status, int skuTypes, long totalQuantity,
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
LocalDateTime createdAt) { }
LocalDateTime createdAt, String relabelStatus, long relabelledQuantity,
long relabelReservedQuantity) { }
public record InventoryItem(String overseasWarehouseId, String cartonMark, String sourceBatchNo, String status, String skuCode,
String productNameZh, long quantity, int cartonSkuTypes, long cartonTotalQuantity,
@@ -23,7 +24,8 @@ public final class ReturnInventoryResponses {
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,
String locationCode, String palletCode, LocalDateTime putawayAt, List<MergeHistory> mergeHistory) { }
String locationCode, String palletCode, LocalDateTime putawayAt, List<MergeHistory> mergeHistory,
long relabelledQuantity, long relabelReservedQuantity) { }
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) {
@@ -57,7 +59,8 @@ public final class ReturnInventoryResponses {
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, String locationCode, String palletCode, LocalDateTime putawayAt) { }
LocalDateTime createdAt, String locationCode, String palletCode, LocalDateTime putawayAt,
long relabelledQuantity, long relabelReservedQuantity) { }
public record PlatformMovement(String customerId, String overseasWarehouseId, String skuCode, String productNameZh,
String movementType, String referenceNo, long totalDelta, long availableDelta,
LocalDateTime occurredAt) { }
@@ -35,6 +35,8 @@ public final class ReturnOrderRequests {
@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String cartonRemark) {}
public record RelabelItem(@NotBlank @Size(max = 128) String skuCode,
@Positive long quantity) {}
@Positive long quantity, @Size(max = 128) String cartonMark) {
public RelabelItem(String skuCode, long quantity) { this(skuCode, quantity, null); }
}
public record Review(@NotBlank String decision, @Size(max = 500) String remark) { }
}
@@ -31,6 +31,8 @@ public class ReturnInventoryCarton {
private LocalDateTime relabelCompletedAt;
private String status;
private String countStatus = "CONFIRMED";
/** WAIT_LABEL until the operator scans the affixed warehouse label; null for historical units. */
private String labelStatus;
private LocalDateTime countedAt;
private String countedBy;
private Integer skuTypes;
@@ -15,7 +15,9 @@ public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventory
@Update("UPDATE ll_return_inventory_balance SET total_quantity=total_quantity-#{quantity}, " +
"available_quantity=available_quantity-#{quantity},updated_at=NOW(3) " +
"WHERE id=#{id} AND customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
"AND total_quantity>=#{quantity} AND available_quantity>=#{quantity} AND carton_quantity>=1")
// PARCEL is a labelled physical unit but is not counted as a box in
// carton_quantity. Piece shipment must therefore only guard quantities.
"AND total_quantity>=#{quantity} AND available_quantity>=#{quantity}")
int deductOutboundPieces(@Param("id") long id, @Param("customerId") String customerId,
@Param("warehouseId") String warehouseId, @Param("quantity") long quantity);
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," +
@@ -10,6 +10,8 @@ import java.util.List;
@Mapper
public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
@Update("UPDATE ll_return_inventory_carton SET relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL WHERE id=#{id}")
int clearRelabel(@Param("id") long id);
@Select("SELECT * FROM ll_return_inventory_carton WHERE source_receipt_id=#{receiptId} ORDER BY id FOR UPDATE")
List<ReturnInventoryCarton> lockReceipt(@Param("receiptId") long receiptId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND status='AVAILABLE' AND count_status='CONFIRMED' ORDER BY carton_mark")
@@ -37,6 +39,9 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
"ORDER BY created_at DESC,id DESC LIMIT 500</script>")
List<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
"AND count_status='CONFIRMED' AND status IN ('AVAILABLE','RELABEL_PENDING','PACKING_RESERVED') ORDER BY created_at DESC,id DESC LIMIT 500")
List<ReturnInventoryCarton> findRelabelCandidates(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1")
ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark);
@@ -57,12 +62,46 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Update("UPDATE ll_return_inventory_carton SET status='AVAILABLE',updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='DISCARD_PENDING'")
int releaseDiscard(@Param("id") long id, @Param("customerId") String customerId);
@Select("SELECT COALESCE(SUM(ri.notified_package_count),0) FROM ll_relabel_item ri " +
String CURRENT_RELABEL_ROUND = " AND (ri.application_id=(SELECT c.relabel_application_id FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) " +
"OR ((SELECT c.relabel_application_id FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) IS NULL " +
"AND NOT EXISTS(SELECT 1 FROM ll_relabel_item newer JOIN ll_relabel_application na ON na.id=newer.application_id " +
"WHERE newer.return_carton_id=ri.return_carton_id AND na.status NOT IN ('CANCELLED','REJECTED') " +
"AND (COALESCE(newer.source_carton_mark,newer.carton_mark)=COALESCE(ri.source_carton_mark,ri.carton_mark) " +
"OR COALESCE(newer.source_carton_mark,newer.carton_mark)=(SELECT c.carton_mark FROM ll_return_inventory_carton c WHERE c.id=#{cartonId})) " +
"AND (na.created_at>ra.created_at OR (na.created_at=ra.created_at AND na.id>ra.id)))))";
String COMPLETION_DEDUCTION = "COALESCE((SELECT SUM(j.quantity) FROM ll_relabel_pda_history h " +
"JOIN JSON_TABLE(CASE WHEN h.action='RETURN_COMPLETION_TRANSFER' AND JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END,'$.items[*]' COLUMNS(sku_code VARCHAR(128) PATH '$.skuCode',quantity BIGINT PATH '$.quantity')) j " +
"WHERE h.action='RETURN_COMPLETION_TRANSFER' AND EXISTS(SELECT 1 FROM ll_relabel_item ri " +
"JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"WHERE ri.return_carton_id=#{cartonId} AND ri.sku_code=#{skuCode} " +
"AND ra.status NOT IN ('CANCELLED','REJECTED')")
"WHERE ri.return_carton_id=#{cartonId} AND JSON_CONTAINS(CASE WHEN JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END," +
"JSON_QUOTE(CAST(ri.id AS CHAR)),'$.sourceItemIds') AND ra.status NOT IN ('CANCELLED','REJECTED') " + CURRENT_RELABEL_ROUND + ")";
@Select("SELECT COALESCE(SUM(CASE WHEN ri.operation_type='RETURN_BULK_SKU_LABEL' THEN f.quantity " +
"ELSE f.quantity * COALESCE((SELECT SUM(j.quantity) FROM JSON_TABLE(CASE WHEN JSON_VALID(ri.sku_relabel_items_json) THEN ri.sku_relabel_items_json ELSE '[]' END, '$[*]' " +
"COLUMNS(sku_code VARCHAR(128) PATH '$.skuCode', quantity BIGINT PATH '$.quantity')) j WHERE UPPER(j.sku_code)=UPPER(#{skuCode})),0) END),0) - " +
COMPLETION_DEDUCTION + " AND UPPER(j.sku_code)=UPPER(#{skuCode})),0) FROM ll_relabel_item ri " +
"JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"JOIN ll_relabel_pda_unit_fact f ON f.item_id=ri.id AND f.result='RELABELLED' " +
"WHERE ri.return_carton_id=#{cartonId} AND (ri.operation_type<>'RETURN_BULK_SKU_LABEL' OR UPPER(ri.sku_code)=UPPER(#{skuCode})) " +
"AND ra.status NOT IN ('CANCELLED','REJECTED')" + CURRENT_RELABEL_ROUND)
long relabelledQuantity(@Param("cartonId") long cartonId, @Param("skuCode") String skuCode);
String RELABEL_ITEM_QUANTITY = "CASE WHEN ri.operation_type='RETURN_BULK_SKU_LABEL' THEN ri.notified_package_count ELSE " +
"COALESCE((SELECT SUM(j.quantity) FROM JSON_TABLE(CASE WHEN JSON_VALID(ri.sku_relabel_items_json) THEN ri.sku_relabel_items_json ELSE '[]' END, '$[*]' " +
"COLUMNS(quantity BIGINT PATH '$.quantity')) j),0) END";
@Select("SELECT COALESCE(SUM(CASE WHEN ri.operation_type='RETURN_BULK_SKU_LABEL' THEN f.quantity ELSE f.quantity * " +
"COALESCE((SELECT SUM(j.quantity) FROM JSON_TABLE(CASE WHEN JSON_VALID(ri.sku_relabel_items_json) THEN ri.sku_relabel_items_json ELSE '[]' END,'$[*]' COLUMNS(quantity BIGINT PATH '$.quantity')) j),0) END),0) - " +
COMPLETION_DEDUCTION + "),0) " +
"FROM ll_relabel_item ri JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"JOIN ll_relabel_pda_unit_fact f ON f.item_id=ri.id AND f.result='RELABELLED' " +
"WHERE ri.return_carton_id=#{cartonId} AND ra.status NOT IN ('CANCELLED','REJECTED')" + CURRENT_RELABEL_ROUND)
long totalRelabelledQuantity(@Param("cartonId") long cartonId);
@Select("SELECT COALESCE(SUM(GREATEST(0,(" + RELABEL_ITEM_QUANTITY + ") - " +
"COALESCE((SELECT SUM(CASE WHEN ri.operation_type='RETURN_BULK_SKU_LABEL' THEN f.quantity ELSE f.quantity * (" +
RELABEL_ITEM_QUANTITY + ") END) FROM ll_relabel_pda_unit_fact f WHERE f.item_id=ri.id AND f.result='RELABELLED'),0))),0) " +
"FROM ll_relabel_item ri JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"WHERE ri.return_carton_id=#{cartonId} AND ra.status NOT IN ('CANCELLED','REJECTED','COMPLETED')")
long relabelReservedQuantity(@Param("cartonId") long cartonId);
@Update("UPDATE ll_return_inventory_carton SET total_quantity=#{remaining},sku_types=#{skuTypes},status='AVAILABLE'," +
"relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL,updated_at=NOW(3) " +
"WHERE id=#{id} AND customer_id=#{customerId} AND relabel_application_id=#{applicationId} " +
@@ -0,0 +1,15 @@
package com.baiyue.lastleg.returnreceipt.mapper;
import org.apache.ibatis.annotations.*;
import java.util.List;
/** Immutable PDA history records exact completed physical pieces moved out of a historical item group. */
@Mapper
public interface ReturnRelabelCompletionLedgerMapper {
@Select("<script>SELECT COALESCE(SUM(j.quantity),0) FROM ll_relabel_pda_history h " +
"JOIN JSON_TABLE(CASE WHEN h.action='RETURN_COMPLETION_TRANSFER' AND JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END,'$.items[*]' COLUMNS(sku_code VARCHAR(128) PATH '$.skuCode',quantity BIGINT PATH '$.quantity')) j " +
"WHERE h.action='RETURN_COMPLETION_TRANSFER' AND UPPER(j.sku_code)=UPPER(#{skuCode}) " +
"AND EXISTS(SELECT 1 FROM JSON_TABLE(CASE WHEN h.action='RETURN_COMPLETION_TRANSFER' AND JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END,'$.sourceItemIds[*]' COLUMNS(item_id VARCHAR(32) PATH '$')) x " +
"WHERE x.item_id IN <foreach item='id' collection='itemIds' open='(' separator=',' close=')'>#{id}</foreach>)</script>")
long deductedQuantity(@Param("itemIds") List<String> itemIds, @Param("skuCode") String skuCode);
}
@@ -56,6 +56,7 @@ public class ReturnInventoryService {
public static final String MERGED = "MERGED";
@Resource private ReturnInventoryCartonMapper cartons;
@Resource private com.baiyue.lastleg.relabel.mapper.RelabelItemMapper relabelItems;
@Resource private ReturnInventoryCartonItemMapper cartonItems;
@Resource private ReturnInventoryBalanceMapper balances;
@Resource private ReturnInventoryOperationMapper operations;
@@ -129,13 +130,27 @@ public class ReturnInventoryService {
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 合箱不能同时选择来源箱");
// SKU mode is the partial-packing mode. It may explicitly name one or
// more physical packages/boxes; the quantities in `boxes` are then
// deducted from those sources and materialized as a new labelled carton.
Map<String, Long> requested = planQuantities(request.boxes());
List<ReturnInventoryRequests.SourceDeduction> exact = request.sourceDeductions();
if (exact != null) {
if (!"SKU".equals(mode) || exact.isEmpty() || marks.isEmpty()) throw badRequest("部分合箱必须指定来源包裹/箱及数量");
var seen = new java.util.HashSet<String>();
for (var line : exact) if (!marks.contains(line.cartonMark()) || line.quantity() <= 0
|| !seen.add(line.cartonMark() + ":" + line.skuCode())) throw badRequest("部分合箱来源重复或不属于所选包裹/箱");
Map<String, Long> totals = new LinkedHashMap<>();
exact.forEach(line -> totals.merge(line.skuCode(), line.quantity(), Math::addExact));
if (!totals.equals(requested)) throw conflict("来源扣除数量必须与目标箱数量一致");
}
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();
: (marks.isEmpty()
? packingSourceCartons(selected).stream()
.sorted(Comparator.comparing(ReturnInventoryCarton::getCartonMark)).map(c -> cartons.lockMark(c.getCartonMark())).toList()
: marks.stream().map(cartons::lockMark).toList());
Map<String, Long> remaining = new LinkedHashMap<>(requested);
for (ReturnInventoryCarton carton : sources) {
if (carton == null || !first.getCustomerId().equals(carton.getCustomerId())
@@ -143,10 +158,12 @@ public class ReturnInventoryService {
|| !AVAILABLE.equals(carton.getStatus())
|| !sourceMatches(carton, returnNos)) throw conflict("来源箱不属于所选退货单或已被占用");
ReturnCartonCountGuard.requireConfirmed(carton);
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 (value(item.getPackingReservedQuantity()) > 0) throw conflict("来源包裹/箱已有合箱预占");
long take = "BOX".equals(mode) ? free : exact == null ? Math.min(free, value(remaining.get(item.getSkuCode())))
: exact.stream().filter(line -> carton.getCartonMark().equals(line.cartonMark()) && item.getSkuCode().equals(line.skuCode())).mapToLong(ReturnInventoryRequests.SourceDeduction::quantity).sum();
if (take > free) throw conflict("来源包裹/箱扣除数量超过可用库存");
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);
@@ -154,7 +171,9 @@ public class ReturnInventoryService {
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.stream().anyMatch(line -> line.cartonMark().equals(carton.getCartonMark()))) {
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);
@@ -282,6 +301,8 @@ public class ReturnInventoryService {
return new ReturnInventoryResponses.PackingPlanCompletion(plan.getPlanNo(), read(plan.getResultLabelsJson(), new TypeReference<List<ReturnInventoryResponses.Label>>() { }));
}
if (!"READY".equals(plan.getStatus())) throw conflict("合箱计划尚未审核或已取消");
if ("SKU".equals(plan.getMode()) && request.boxes().stream().anyMatch(box -> text(box.targetCartonMark()) != null))
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("合箱计划缺少来源锁定明细,请重新创建");
@@ -289,7 +310,7 @@ public class ReturnInventoryService {
? 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) {
if (request.sourceDeductions() != null) {
Map<String, Long> seen = new LinkedHashMap<>();
List<SourceItem> selected = new ArrayList<>();
for (var deduction : request.sourceDeductions()) {
@@ -299,8 +320,13 @@ public class ReturnInventoryService {
selected.add(new SourceItem(deduction.cartonMark(), deduction.skuCode(), deduction.quantity()));
}
sourceAllocation = selected;
if ("SKU".equals(plan.getMode()) && !new java.util.HashSet<>(sourceAllocation).equals(new java.util.HashSet<>(allocation)))
throw conflict("部分合箱实际来源与计划锁定来源数量不一致");
}
final List<SourceItem> selectedAllocation = sourceAllocation;
if (plannedTarget != null && allocation.stream().anyMatch(source -> !plannedTarget.equals(source.cartonMark())
&& selectedAllocation.stream().filter(s -> s.cartonMark().equals(source.cartonMark()) && s.skuCode().equals(source.skuCode())).mapToLong(SourceItem::quantity).sum() != source.quantity()))
throw conflict("部分包裹/箱必须合箱生成独立新箱,不能并入已有箱");
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<>();
@@ -464,8 +490,8 @@ public class ReturnInventoryService {
required(lines.get(0).skuCode(), "SKU 不能为空");
String cartonMark = text(requestedMark);
long cartonId = IdWorker.getId();
// A parcel row tracks loose SKU provenance; it is never a physical carton or printable box label.
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "SP" + hash(List.of(receipt.getId(), lines.get(0).skuCode())).substring(0, 24).toUpperCase(java.util.Locale.ROOT);
// One durable barcode identifies a physical parcel, irrespective of SKU count.
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "RP" + hash(List.of(receipt.getId())).substring(0, 24).toUpperCase(java.util.Locale.ROOT);
else if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
if (cartons.findMark(cartonMark) != null) throw conflict("箱号已存在: " + cartonMark);
LocalDateTime now = LocalDateTime.now();
@@ -476,10 +502,11 @@ public class ReturnInventoryService {
carton.setSourceReturnNo(order.getReturnNo()); carton.setCartonMark(cartonMark);
carton.setPalletCode(text(receipt.getPalletCode()));
carton.setSourceUnitType(receipt.getUnitType());
carton.setLabelStatus("PALLET".equals(receipt.getUnitType()) ? null : "WAIT_LABEL");
carton.setCountStatus("PALLET".equals(receipt.getUnitType()) ? "PENDING" : "CONFIRMED");
carton.setSourceUnitCode(normalizedUnitCode);
// Pallet measurements describe the carrier, never each child carton.
if ("BOX".equals(receipt.getUnitType())) {
if ("BOX".equals(receipt.getUnitType()) || "PARCEL".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);
@@ -492,7 +519,7 @@ public class ReturnInventoryService {
balances.adjust(IdWorker.getId(), order.getCustomerId(), order.getOverseasWarehouseId(), line.skuCode(),
text(line.productNameZh()), line.quantity(), line.quantity(), "PARCEL".equals(receipt.getUnitType()) ? 0 : 1);
}
if ("BOX".equals(receipt.getUnitType())) {
if ("BOX".equals(receipt.getUnitType()) || "PARCEL".equals(receipt.getUnitType())) {
var contents = lines.stream().map(line -> new com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Sku(line.skuCode(), line.productNameZh(), line.quantity())).toList();
var snapshot = new com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Task(receipt.getReceiptNo(), order.getReturnNo(), receipt.getPalletCode(), "CONFIRMED", contents,
List.of(new com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Box(cartonMark, contents)));
@@ -551,7 +578,8 @@ public class ReturnInventoryService {
.map(carton -> new ReturnInventoryResponses.CartonSummary(carton.getCartonMark(), carton.getOverseasWarehouseId(),
carton.getSourceBatchNo(), carton.getSourceReturnNo(), displayStatus(carton),
carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()),
carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt())).toList();
carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(),
relabelProgressStatus(carton), cartons.totalRelabelledQuantity(carton.getId()), cartons.relabelReservedQuantity(carton.getId()))).toList();
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
}
@@ -594,8 +622,9 @@ public class ReturnInventoryService {
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.getSourceUnitType(), carton.getSourceBatchNo(),
displayStatus(carton), 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(), carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt())).toList();
displayStatus(carton), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), relabelProgressStatus(carton), carton.getLength(),
carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt(),
cartons.totalRelabelledQuantity(carton.getId()), cartons.relabelReservedQuantity(carton.getId()))).toList();
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
}
@@ -652,10 +681,20 @@ public class ReturnInventoryService {
return label(carton);
}
@Transactional
public ReturnInventoryResponses.Label confirmLabel(PdaWarehouseIdentity identity, String cartonMark) {
ReturnInventoryCarton carton = cartons.lockMark(required(cartonMark, "请扫描已贴好的包裹/箱标签"));
if (carton == null || !String.valueOf(identity.overseasWarehouseId()).equals(carton.getOverseasWarehouseId())) throw notFound("当前仓没有该包裹/箱");
if (!AVAILABLE.equals(carton.getStatus())) throw conflict("已占用或已拆空的包裹/箱不能确认贴标");
if (!"CONFIRMED".equals(carton.getLabelStatus())) {
carton.setLabelStatus("CONFIRMED"); carton.setUpdatedAt(LocalDateTime.now()); cartons.updateById(carton);
}
return label(carton);
}
/** Print only persisted physical cartons; a parcel's SKU rows are not cartons. */
@Transactional(readOnly = true)
public List<ReturnOrderResponses.ReceiptCarton> receiptContents(ReturnReceipt receipt) {
if ("PARCEL".equals(receipt.getUnitType())) return List.of();
var snapshot = operations.receiptContents(receipt.getCustomerId(), receipt.getId());
if (snapshot != null) {
var task = read(snapshot.getResultJson(), new TypeReference<com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Task>() { });
@@ -673,7 +712,6 @@ public class ReturnInventoryService {
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) {
@@ -831,7 +869,7 @@ public class ReturnInventoryService {
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("该箱不是可用实物箱");
if (!AVAILABLE.equals(carton.getStatus())) throw conflict("该包裹/箱不是可用实物单元");
ReturnCartonCountGuard.requireConfirmed(carton);
return cartonDetail(carton);
}
@@ -851,7 +889,7 @@ public class ReturnInventoryService {
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())
if (carton == null || !AVAILABLE.equals(carton.getStatus())
|| !String.valueOf(identity.overseasWarehouseId()).equals(carton.getOverseasWarehouseId())) throw conflict("来源箱不存在或不可合并");
if (customer != null && !customer.equals(carton.getCustomerId())) throw conflict("不同客户的箱不能合并");
ReturnCartonCountGuard.requireConfirmed(carton);
@@ -868,6 +906,10 @@ public class ReturnInventoryService {
}
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.sourceCartonMarks().stream().anyMatch(mark ->
cartonItems.lockCarton(cartons.lockMark(mark).getId()).stream().anyMatch(item ->
value(limits.get(mark).get(item.getSkuCode())) != value(item.getQuantity()))))
throw conflict("部分包裹/箱必须合箱生成独立新箱,不能并入已有箱");
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;
@@ -956,7 +998,7 @@ public class ReturnInventoryService {
Map<String, Long> requested = new LinkedHashMap<>();
Map<String, String> productNames = new LinkedHashMap<>();
for (ReturnInventoryRequests.ValidItem item : request.items()) {
String sku = required(item.skuCode(), "SKU 不能为空").toUpperCase(java.util.Locale.ROOT);
String sku = required(item.skuCode(), "SKU 不能为空");
if (requested.put(sku, item.quantity()) != null) throw badRequest("合箱 SKU 不能重复");
}
Map<String, Long> remaining = new LinkedHashMap<>(requested);
@@ -965,13 +1007,21 @@ public class ReturnInventoryService {
LinkedHashSet<String> consumedReturnNos = new LinkedHashSet<>();
Map<Long, Map<String, Long>> attachedByOrder = new LinkedHashMap<>();
List<TraceTransfer> transfers = new ArrayList<>();
java.util.Set<Long> completedRelabelSources = new java.util.HashSet<>();
long total = 0;
for (ReturnInventoryCarton source : sources) {
// Read committed quantities under the source lock, never a stale MVCC snapshot.
List<ReturnInventoryCartonItem> sourceItems = cartonItems.lockCarton(source.getId());
boolean hasCompletedRelabel = sourceItems.stream().anyMatch(i -> cartons.relabelledQuantity(source.getId(), i.getSkuCode()) > 0);
if (hasCompletedRelabel) completedRelabelSources.add(source.getId());
if (hasCompletedRelabel && source.getRelabelApplicationId() == null && sourceItems.stream().anyMatch(i -> {
long free = Math.max(0, value(i.getQuantity()) - value(i.getPackingReservedQuantity()));
long allowed = limits == null ? free : Math.min(free, value(limits.getOrDefault(source.getCartonMark(), Map.of()).get(i.getSkuCode())));
return Math.min(value(remaining.get(i.getSkuCode())), allowed) != value(i.getQuantity());
})) throw conflict("已换标与未换标货物不能自动分配来源,请先在换标任务内隔离为独立新箱,或整箱合并");
long sourceTotal = 0; int sourceTypes = 0;
for (ReturnInventoryCartonItem item : sourceItems) {
String sku = item.getSkuCode().toUpperCase(java.util.Locale.ROOT);
String sku = item.getSkuCode();
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);
@@ -1016,6 +1066,8 @@ public class ReturnInventoryService {
}
target.setSourceBatchNo(String.join(",", consumedBatchNos)); target.setSourceReturnNo(String.join(",", consumedReturnNos)); target.setStatus(AVAILABLE);
if (existingTarget == null) {
target.setCountStatus("CONFIRMED");
target.setLabelStatus("WAIT_LABEL");
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);
}
@@ -1037,6 +1089,8 @@ public class ReturnInventoryService {
}
for (TraceTransfer transfer : transfers) packingTrace.insert(IdWorker.getId(), customerId, operation.getId(),
transfer.source(), target.getCartonMark(), transfer.returnNo(), transfer.sku(), transfer.quantity(), now);
for (ReturnInventoryCarton source : sources) if (completedRelabelSources.contains(source.getId()) && MERGED.equals(source.getStatus()) && source.getRelabelApplicationId() == null)
relabelItems.moveCompletedReturnCarton(customerId, source.getId(), targetId, target.getCartonMark());
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;
@@ -1111,7 +1165,12 @@ public class ReturnInventoryService {
"AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
? 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(),
displayStatus(carton), 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()));
displayStatus(carton), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), relabelProgressStatus(carton), 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()),
cartons.totalRelabelledQuantity(carton.getId()), cartons.relabelReservedQuantity(carton.getId()));
}
private String relabelProgressStatus(ReturnInventoryCarton carton) {
return ReturnRelabelProgress.status(value(carton.getTotalQuantity()), cartons.totalRelabelledQuantity(carton.getId()), cartons.relabelReservedQuantity(carton.getId()));
}
private static String printableHtml(ReturnInventoryCarton carton) {
@@ -278,7 +278,7 @@ public class ReturnReceiptService {
.toList();
long boxes = value(receipt.getTotalBoxCount());
if (boxes == 0) boxes = lines.stream().mapToLong(ReturnOrderResponses.ReceiptItem::boxCount).sum();
String cartonMark = "PARCEL".equals(receipt.getUnitType()) ? null : text(receipt.getCartonMark());
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();
@@ -382,13 +382,12 @@ public class ReturnReceiptService {
if (order == null) throw notFound("退货单不存在");
if (!COMPLETED.equals(order.getStatus())) throw conflict("退货单尚未完成签收");
List<ReturnOrderResponses.RelabelCandidate> result = new ArrayList<>();
for (ReturnReceipt receipt : receipts.listOrder(order.getId())) {
for (ReturnInventoryCarton carton : cartons.listReceipt(receipt.getId())) {
for (ReturnInventoryCarton carton : cartons.findAvailable(customerId, order.getOverseasWarehouseId())) {
if (!inventory.matchesReturnSource(carton, order.getReturnNo())) continue;
if (!ReturnInventoryService.AVAILABLE.equals(carton.getStatus()) || !"CONFIRMED".equals(carton.getCountStatus()) || 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;
}
@@ -419,9 +418,9 @@ public class ReturnReceiptService {
long totalQuantity = 0;
for (ReturnOrderRequests.RelabelItem requested : request.items()) {
String sku = norm(requested.skuCode());
if (!seen.add(sku)) throw badRequest("同一贴标申请不能重复选择 SKU: " + requested.skuCode());
if (!seen.add(sku + ":" + text(requested.cartonMark()))) throw badRequest("同一包裹/箱 SKU 不能重复选择: " + requested.skuCode());
ReturnOrderItem received = receivedBySku.get(sku);
if (received == null || requested.quantity() > value(received.getReceivedQuantity())) {
if (text(requested.cartonMark()) == null && (received == null || requested.quantity() > value(received.getReceivedQuantity()))) {
throw conflict("SKU " + requested.skuCode() + " 的贴标数量超过当前退货单实收数量");
}
totalQuantity = Math.addExact(totalQuantity, requested.quantity());
@@ -460,27 +459,35 @@ public class ReturnReceiptService {
app.setFulfillmentStatus("WAIT_APPROVAL"); app.setRemark(text(request.cartonRemark())); app.setStatus("PENDING_REVIEW"); app.setItemCount(request.items().size()); app.setTotalPackageCount(Math.toIntExact(totalQuantity)); app.setNotifiedPackageCount(Math.toIntExact(totalQuantity)); app.setChargeAmount(java.math.BigDecimal.ZERO); app.setChargeCurrency("CNY"); app.setWalletStatus("NOT_REQUIRED"); app.setIdempotencyKey(key); app.setRequestHash(requestHash); app.setCreatedBy(text(operatorId)); app.setCreatedAt(now); app.setUpdatedAt(now); relabelApplications.insert(app);
List<ReturnInventoryCarton> availableCartons = cartons.findAvailable(customerId, order.getOverseasWarehouseId());
Map<Long, ReturnInventoryCarton> selectedCartons = new LinkedHashMap<>();
Map<List<String>, Long> allocatedBySourceSku = new LinkedHashMap<>();
for (ReturnOrderRequests.RelabelItem requested : request.items()) {
ReturnOrderItem source = receivedBySku.get(norm(requested.skuCode()));
long remaining = requested.quantity();
for (ReturnInventoryCarton candidate : availableCartons) {
if (remaining == 0) break;
if (!order.getReturnNo().equals(candidate.getSourceReturnNo())) continue;
if (!inventory.matchesReturnSource(candidate, order.getReturnNo())) continue;
if (text(requested.cartonMark()) != null && !requested.cartonMark().equals(candidate.getCartonMark())) continue;
long skuQuantity = cartonItems.listCarton(candidate.getId()).stream()
.filter(line -> norm(requested.skuCode()).equals(norm(line.getSkuCode())))
.mapToLong(line -> value(line.getQuantity())).sum();
skuQuantity -= cartons.relabelledQuantity(candidate.getId(), norm(requested.skuCode()));
if (skuQuantity <= 0) continue;
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 已被合箱计划锁定,请先完成或取消合箱计划");
skuQuantity = cartonItems.lockCarton(carton.getId()).stream()
.filter(line -> norm(requested.skuCode()).equals(norm(line.getSkuCode())))
.mapToLong(line -> value(line.getQuantity())).sum();
var sourceSku = List.of(carton.getCartonMark(), norm(requested.skuCode()));
skuQuantity -= allocatedBySourceSku.getOrDefault(sourceSku, 0L);
if (skuQuantity <= 0) continue;
long allocated = Math.min(remaining, skuQuantity);
allocatedBySourceSku.merge(sourceSku, allocated, Math::addExact);
ReturnCartonCountGuard.requireConfirmed(carton);
RelabelItem item = new RelabelItem(); item.setId(IdWorker.getId()); item.setApplicationId(appId); item.setCustomerId(customerId);
item.setSkuCode(source.getSkuCode());
item.setProductNameZh(text(source.getProductNameZh()) == null ? source.getSkuCode() : text(source.getProductNameZh()));
item.setSkuCode(requested.skuCode());
item.setProductNameZh(source == null || text(source.getProductNameZh()) == null ? requested.skuCode() : text(source.getProductNameZh()));
item.setTotalPackageCount(Math.toIntExact(allocated)); item.setNotifiedPackageCount(Math.toIntExact(allocated));
item.setShipmentNo(order.getReturnNo()); item.setLabelRequirement(text(request.cartonRemark()));
item.setDestinationWarehouseId(String.valueOf(destination.id())); item.setDestinationFbaWarehouseCode(destination.code());
@@ -653,13 +660,12 @@ public class ReturnReceiptService {
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)));
// One receipt represents one physical parcel, including all its SKUs.
inventoryCartons = List.of(inventory.createReceiptCarton(order, receipt, null, new ArrayList<>(inventoryLines.values())));
} else {
inventoryCartons = List.of(inventory.createReceiptCarton(order, receipt, unitCode, new ArrayList<>(inventoryLines.values())));
}
receipt.setCartonMark("PARCEL".equals(unitType) ? null : inventoryCartons.get(0).getCartonMark());
receipt.setCartonMark(inventoryCartons.get(0).getCartonMark());
receipts.updateById(receipt);
syncTailCargo(order, receipt, allocations, now);
List<ReturnOrderItem> finalItems = orderItems.listOrder(order.getId());
@@ -802,10 +808,10 @@ public class ReturnReceiptService {
return confirmed(order, receipt, COMPLETED.equals(order.getStatus()));
}
private PdaReturnReceiptResponses.Confirmed confirmed(ReturnOrder order, ReturnReceipt receipt, boolean completed) {
String cartonMark = "PARCEL".equals(receipt.getUnitType()) ? null : text(receipt.getCartonMark());
String cartonMark = text(receipt.getCartonMark());
String printableHtml = null;
List<ReturnOrderResponses.CartonLabel> labels = new java.util.ArrayList<>();
if (receipt.getId() != null && !"PARCEL".equals(receipt.getUnitType())) {
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()));
@@ -0,0 +1,11 @@
package com.baiyue.lastleg.returnreceipt.service;
/** Physical piece progress is independent of warehouse identification-label confirmation. */
public final class ReturnRelabelProgress {
private ReturnRelabelProgress() { }
public static String status(long total, long completed, long reserved) {
if (total > 0 && completed >= total) return "COMPLETED";
if (completed > 0) return "PARTIALLY_COMPLETED";
return reserved > 0 ? "PROCESSING" : "UNLABELLED";
}
}