贴标申请,仓储转运

This commit is contained in:
2026-10-13 14:46:22 +08:00
parent 89a05a2348
commit c84b5f460a
4 changed files with 49 additions and 7 deletions
@@ -44,7 +44,8 @@ public final class RelabelResponses {
String sourceType, Long returnCartonId, String cartonMark, String sourceType, Long returnCartonId, String cartonMark,
List<SkuItem> skuItems, long relabelledPackages, String relabelStatus, List<SkuItem> skuItems, long relabelledPackages, String relabelStatus,
long relabelledQuantity, long relabelReservedQuantity, Long stockQuantity, String inventoryStatus, String occupiedRelabelNo, List<String> mergedIntoCartonMarks, boolean hasOutbound, long relabelledQuantity, long relabelReservedQuantity, Long stockQuantity, String inventoryStatus, String occupiedRelabelNo, List<String> mergedIntoCartonMarks, boolean hasOutbound,
List<com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion> skuConversions) { List<com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion> skuConversions,
List<SkuItem> cartonSkuItems, long cartonAvailablePackages) {
public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode, public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode,
String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName, String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName,
String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh, String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh,
@@ -53,7 +54,7 @@ public final class RelabelResponses {
this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId, this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId,
overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName, overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName,
skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark, skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark,
skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false, List.of()); skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false, List.of(), List.of(), 0);
} }
} }
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo, public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
@@ -284,7 +284,13 @@ public class RelabelApplicationService implements WalletResultHandler {
return new RelabelResponses.SkuItem(item.getSkuCode(), null, return new RelabelResponses.SkuItem(item.getSkuCode(), null,
Math.toIntExact(Math.max(remaining, 0)), null); Math.toIntExact(Math.max(remaining, 0)), null);
}).filter(item -> !"AVAILABLE".equals(carton.getStatus()) || item.quantity() > 0).toList(); }).filter(item -> !"AVAILABLE".equals(carton.getStatus()) || item.quantity() > 0).toList();
if (cartonSkus.isEmpty() && !java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus())) continue; List<RelabelResponses.SkuItem> physicalSkus = cartonLines.stream()
.filter(line -> value(line.getQuantity()) > 0)
.map(line -> new RelabelResponses.SkuItem(line.getSkuCode(), null, Math.toIntExact(value(line.getQuantity())), null)).toList();
long cartonAvailable = "AVAILABLE".equals(carton.getStatus()) && carton.getRelabelApplicationId() == null
&& cartonLines.stream().noneMatch(line -> value(line.getPackingReservedQuantity()) > 0)
&& !physicalSkus.isEmpty() ? 1 : 0;
if (cartonSkus.isEmpty() && cartonAvailable == 0 && !java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus())) continue;
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode) String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum(); long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
@@ -301,7 +307,7 @@ public class RelabelApplicationService implements WalletResultHandler {
.filter(history -> carton.getCartonMark().equals(history.sourceCartonMark())) .filter(history -> carton.getCartonMark().equals(history.sourceCartonMark()))
.map(history -> history.targetCartonMark()).distinct().toList(), returnCartons.hasOutbound(carton.getId()), .map(history -> history.targetCartonMark()).distinct().toList(), returnCartons.hasOutbound(carton.getId()),
skuConversionRows.listCarton(customerId, carton.getId()).stream() skuConversionRows.listCarton(customerId, carton.getId()).stream()
.map(com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion::from).toList())); .map(com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion::from).toList(), physicalSkus, cartonAvailable));
} }
return result; return result;
} }
@@ -414,7 +420,8 @@ public class RelabelApplicationService implements WalletResultHandler {
final long sourceReturnCartonId = carton.getId(); final long sourceReturnCartonId = carton.getId();
Map<String, Long> availableSku = returnCartonItems.lockCarton(sourceReturnCartonId).stream() Map<String, Long> availableSku = returnCartonItems.lockCarton(sourceReturnCartonId).stream()
.collect(Collectors.toMap(line -> line.getSkuCode().toUpperCase(java.util.Locale.ROOT), .collect(Collectors.toMap(line -> line.getSkuCode().toUpperCase(java.util.Locale.ROOT),
line -> remainingRelabelQuantity(sourceReturnCartonId, line), Long::sum)); line -> "CARTON_LABEL".equals(operationType(item)) ? value(line.getQuantity())
: remainingRelabelQuantity(sourceReturnCartonId, line), Long::sum));
if (availableSku.values().stream().noneMatch(quantity -> quantity > 0)) if (availableSku.values().stream().noneMatch(quantity -> quantity > 0))
throw conflict("来源箱 SKU 已全部换标,没有剩余可换标数量"); throw conflict("来源箱 SKU 已全部换标,没有剩余可换标数量");
Map<String, Long> requestedSku = selectedReturnSkus.computeIfAbsent(carton.getId(), ignored -> new java.util.HashMap<>()); Map<String, Long> requestedSku = selectedReturnSkus.computeIfAbsent(carton.getId(), ignored -> new java.util.HashMap<>());
@@ -3110,6 +3110,15 @@ components:
RelabelInventoryCandidate: RelabelInventoryCandidate:
type: object type: object
properties: properties:
cartonAvailablePackages: { type: integer, format: int64, minimum: 0, maximum: 1, description: 仅换箱唛可选择的当前实物箱数;已换 SKU 不影响可选箱数,占用和合箱预留仍不可选 }
cartonSkuItems:
type: array
description: 当前实物箱内全部 SKU 和件数,包含换标后的新 SKU
items:
type: object
properties:
skuCode: { type: string }
quantity: { type: integer, minimum: 1 }
skuConversions: { type: array, items: { $ref: '#/components/schemas/ReturnSkuConversion' }, description: 当前实物关联的旧新SKU转换、实际件数和客户确认状态 } skuConversions: { type: array, items: { $ref: '#/components/schemas/ReturnSkuConversion' }, description: 当前实物关联的旧新SKU转换、实际件数和客户确认状态 }
relabelStatus: { type: string, nullable: true, enum: [UNLABELLED, PROCESSING, PARTIALLY_COMPLETED, COMPLETED], description: 退货实物按实际完成件数和未完成申请占用计算 } relabelStatus: { type: string, nullable: true, enum: [UNLABELLED, PROCESSING, PARTIALLY_COMPLETED, COMPLETED], description: 退货实物按实际完成件数和未完成申请占用计算 }
relabelledPackages: { type: integer, format: int64, description: 退货实际已完成商品换标件数,不从可用库存差值推导 } relabelledPackages: { type: integer, format: int64, description: 退货实际已完成商品换标件数,不从可用库存差值推导 }
@@ -109,11 +109,35 @@ class RelabelApplicationServiceTest {
assertEquals(4L, candidate.stockQuantity()); assertEquals(4L, candidate.stockQuantity());
assertEquals("AVAILABLE", candidate.inventoryStatus()); assertEquals("AVAILABLE", candidate.inventoryStatus());
assertEquals(List.of(), candidate.mergedIntoCartonMarks()); assertEquals(List.of(), candidate.mergedIntoCartonMarks());
assertEquals(1, candidate.cartonAvailablePackages());
carton.setStatus("RELABEL_PENDING"); carton.setStatus("RELABEL_PENDING");
var occupied = service.candidates("C", "W", null).get(0); var occupied = service.candidates("C", "W", null).get(0);
assertEquals(0, occupied.availablePackages()); assertEquals(0, occupied.availablePackages());
assertEquals(0, occupied.cartonAvailablePackages());
assertEquals(2, occupied.skuItems().size(), "处理中库存保留占用信息,但不可再次选择"); assertEquals(2, occupied.skuItems().size(), "处理中库存保留占用信息,但不可再次选择");
} }
@Test void convertedSkuRemainsAvailableForCartonLabelOnly() {
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
ReflectionTestUtils.setField(service, "returnCartons", cartons);
ReflectionTestUtils.setField(service, "returnCartonItems", lines);
ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
carton.setId(20L); carton.setStatus("AVAILABLE"); carton.setCartonMark("NEW-BOX"); carton.setTotalQuantity(15L);
var first = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
first.setSkuCode("NEW-A"); first.setQuantity(10L); first.setRelabelledQuantity(10L);
var second = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
second.setSkuCode("NEW-B"); second.setQuantity(5L); second.setRelabelledQuantity(5L);
when(cartons.findRelabelCandidates("C", "W")).thenReturn(List.of(carton));
when(lines.listCarton(20L)).thenReturn(List.of(first, second));
var candidate = service.candidates("C", "W", null).get(0);
assertEquals(0, candidate.availablePackages());
assertTrue(candidate.skuItems().isEmpty());
assertEquals("NEW-BOX", candidate.cartonMark());
assertEquals(1, candidate.cartonAvailablePackages());
assertEquals(List.of(new RelabelResponses.SkuItem("NEW-A", null, 10, null),
new RelabelResponses.SkuItem("NEW-B", null, 5, null)), candidate.cartonSkuItems());
}
@org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(longs = {0, 2, 5, 8}) @org.junit.jupiter.params.provider.ValueSource(longs = {0, 2, 5, 8})
void returnCandidatesOnlyOfferUnlabelledPieces(long completed) { void returnCandidatesOnlyOfferUnlabelledPieces(long completed) {
@@ -1066,7 +1090,7 @@ class RelabelApplicationServiceTest {
} }
@org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.CsvSource({"CARTON_LABEL,0", "SKU_LABEL,0", "SKU_LABEL,2", "CARTON_AND_SKU,0"}) @org.junit.jupiter.params.provider.CsvSource({"CARTON_LABEL,0", "CARTON_LABEL,5", "SKU_LABEL,0", "SKU_LABEL,2", "CARTON_AND_SKU,0"})
void intactParcelSupportsAllLabelOperationsWithoutMergingIntoBox(String type, long completed) { void intactParcelSupportsAllLabelOperationsWithoutMergingIntoBox(String type, long completed) {
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class); var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class); var lines = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper.class);
@@ -1077,7 +1101,8 @@ class RelabelApplicationServiceTest {
parcel.setCountStatus("CONFIRMED"); parcel.setTotalQuantity(5L); parcel.setCountStatus("CONFIRMED"); parcel.setTotalQuantity(5L);
var line = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); line.setSkuCode("SKU-A"); line.setQuantity(5L); var line = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem(); line.setSkuCode("SKU-A"); line.setQuantity(5L);
when(cartons.lockId(901L)).thenReturn(parcel); when(lines.lockCarton(901L)).thenReturn(List.of(line)); when(cartons.lockId(901L)).thenReturn(parcel); when(lines.lockCarton(901L)).thenReturn(List.of(line));
when(cartons.completedSkuQuantity(901L, "SKU-A")).thenReturn(completed); if ("CARTON_LABEL".equals(type)) line.setRelabelledQuantity(completed);
else when(cartons.completedSkuQuantity(901L, "SKU-A")).thenReturn(completed);
when(cartons.markRelabelPending(anyLong(), anyLong(), anyString(), any())).thenReturn(1); when(cartons.markRelabelPending(anyLong(), anyLong(), anyString(), any())).thenReturn(1);
var app = application(11L, "RL-PARCEL", "10001", "parcel-key", null, "PENDING_REVIEW"); app.setLastlegOrderId(21L); var app = application(11L, "RL-PARCEL", "10001", "parcel-key", null, "PENDING_REVIEW"); app.setLastlegOrderId(21L);
when(applications.findNo(eq("10001"), any())).thenReturn(app); when(applications.findNo(eq("10001"), any())).thenReturn(app);