贴标申请,仓储转运
This commit is contained in:
+14
-42
@@ -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) {}
|
||||
|
||||
+19
@@ -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);
|
||||
|
||||
+7
@@ -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) {
|
||||
|
||||
+14
-2
@@ -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,
|
||||
|
||||
+26
-2
@@ -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,
|
||||
|
||||
+46
-2
@@ -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";
|
||||
}
|
||||
|
||||
+27
-23
@@ -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;
|
||||
}
|
||||
|
||||
+17
-3
@@ -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()))
|
||||
|
||||
+136
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
@@ -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) {
|
||||
|
||||
+6
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -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) { }
|
||||
|
||||
+3
-1
@@ -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) { }
|
||||
}
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
+3
-1
@@ -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," +
|
||||
|
||||
+42
-3
@@ -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} " +
|
||||
|
||||
+15
@@ -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);
|
||||
}
|
||||
+80
-21
@@ -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) {
|
||||
|
||||
+22
-16
@@ -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()));
|
||||
|
||||
+11
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user