贴标申请,仓储转运
This commit is contained in:
+3
-1
@@ -258,11 +258,13 @@ public interface DispatchCandidateMapper {
|
||||
@Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("<script>SELECT a.customer_id AS customerId,a.lastleg_order_id AS orderId,a.overseas_warehouse_id AS overseasWarehouseId," +
|
||||
"MIN(ri.source_warehouse_name) AS overseasWarehouseName,a.relabel_no AS orderNo,a.relabel_no AS orderRefs," +
|
||||
"MIN(ri.source_warehouse_name) AS overseasWarehouseName,'RETURN_RELABEL' AS orderType,a.relabel_no AS orderNo,a.relabel_no AS orderRefs," +
|
||||
"GROUP_CONCAT(DISTINCT c.source_return_no ORDER BY c.source_return_no SEPARATOR ',') AS inboundCode," +
|
||||
"c.pallet_code AS palletNo,COUNT(DISTINCT c.id) AS packageCount,'RELABEL' AS sourceStage,1 AS returnCartonSource," +
|
||||
"GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') AS relabelCartonMarks," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
|
||||
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(x.weight) THEN SUM(x.weight) END FROM (SELECT DISTINCT c2.id,c2.weight FROM ll_return_inventory_carton c2 JOIN ll_relabel_item ri2 ON ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id JOIN ll_relabel_application a2 ON a2.id=ri2.application_id AND a2.customer_id=ri2.customer_id WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND a2.id=a.id) x),(SELECT rr.weight FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS grossWeightKg," +
|
||||
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(x.volume) THEN SUM(x.volume) END FROM (SELECT DISTINCT c2.id,(c2.length*c2.width*c2.height)/1000000 AS volume FROM ll_return_inventory_carton c2 JOIN ll_relabel_item ri2 ON ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id JOIN ll_relabel_application a2 ON a2.id=ri2.application_id AND a2.customer_id=ri2.customer_id WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND a2.id=a.id) x),(SELECT rr.length*rr.width*rr.height/1000000 FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS volumeCbm," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode " +
|
||||
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
|
||||
"JOIN ll_return_inventory_carton c ON c.id=ri.return_carton_id AND c.customer_id=a.customer_id " +
|
||||
|
||||
+8
-1
@@ -25,7 +25,14 @@ public final class PalletOperationResponses {
|
||||
String palletCode, String status, int palletVersion,
|
||||
@JsonSerialize(using = ToStringSerializer.class) Long locationId,
|
||||
String locationCode, String destinationWarehouseCode,
|
||||
long totalPackages, List<InventoryLine> inventory) { }
|
||||
long totalPackages, List<InventoryLine> inventory,
|
||||
java.math.BigDecimal grossWeightKg, java.math.BigDecimal volumeCbm) {
|
||||
public PalletContent(long palletId, String palletCode, String status, int palletVersion, Long locationId,
|
||||
String locationCode, String destinationWarehouseCode, long totalPackages, List<InventoryLine> inventory) {
|
||||
this(palletId, palletCode, status, palletVersion, locationId, locationCode, destinationWarehouseCode,
|
||||
totalPackages, inventory, null, null);
|
||||
}
|
||||
}
|
||||
public record PrintablePallet(@JsonSerialize(using = ToStringSerializer.class) long palletId,
|
||||
String palletCode, int palletVersion,
|
||||
String relabelNo, String printableHtml) { }
|
||||
|
||||
+19
-4
@@ -316,7 +316,8 @@ public class PdaPalletOperationService {
|
||||
PalletOperationRequests.RelabelDisassemble request) {
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
|
||||
Set<String> alreadyBound = boundRelabelMarks(application);
|
||||
if (selected.stream().map(RelabelItem::getCartonMark).anyMatch(alreadyBound::contains))
|
||||
boolean rebind = request.actualSource() == null && selected.stream().allMatch(item -> alreadyBound.contains(item.getCartonMark()));
|
||||
if (!rebind && selected.stream().map(RelabelItem::getCartonMark).anyMatch(alreadyBound::contains))
|
||||
throw conflict("退货换标箱已在本轮完成组托,请刷新后重试");
|
||||
long quantity = 0;
|
||||
List<ReturnInventoryCarton> cartons = new ArrayList<>();
|
||||
@@ -330,8 +331,8 @@ public class PdaPalletOperationService {
|
||||
quantity += value(item.getNotifiedPackageCount());
|
||||
cartons.add(carton);
|
||||
}
|
||||
if (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|
||||
|| request.actualSource().packages() != quantity)
|
||||
if (!rebind && (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|
||||
|| request.actualSource().packages() != quantity))
|
||||
throw conflict("退货换标拆出数量必须等于本批已完成贴标数量");
|
||||
String key = required(request.idempotencyKey(), "幂等键不能为空");
|
||||
String requestHash = hash(request);
|
||||
@@ -836,6 +837,8 @@ public class PdaPalletOperationService {
|
||||
Set<String> destinations = new HashSet<>();
|
||||
selected.forEach(item -> destinations.add(text(item.getDestinationFbaWarehouseCode())));
|
||||
if (destinations.size() != 1 || destinations.contains("")) throw conflict("请按已维护的新目的仓分批组托");
|
||||
returnCartons.palletCartons(String.valueOf(warehouseId), target.palletCode())
|
||||
.forEach(carton -> destinations.add(text(carton.getDestinationWarehouseCode())));
|
||||
for (PalletInventoryRow row : mapper.content(String.valueOf(warehouseId), target.palletId())) {
|
||||
var bound = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("customer_id", row.getCustomerId()).eq("source_inventory_position_id", row.getPositionId()));
|
||||
@@ -898,10 +901,22 @@ public class PdaPalletOperationService {
|
||||
value(row.getReceivingPackages()), value(row.getVersion()));
|
||||
}).toList();
|
||||
long total = rows.stream().mapToLong(PdaPalletOperationService::rowPackages).sum();
|
||||
var boxes = returnCartons.palletCartons(String.valueOf(warehouseId), pallet.palletCode());
|
||||
boxes.forEach(box -> destinations.add(text(box.getDestinationWarehouseCode())));
|
||||
total += boxes.size();
|
||||
// Derive totals from current physical bindings, so retries and rebinds cannot double count.
|
||||
java.math.BigDecimal weight = rows.isEmpty() ? java.math.BigDecimal.ZERO : null;
|
||||
java.math.BigDecimal volume = rows.isEmpty() ? java.math.BigDecimal.ZERO : null;
|
||||
for (var box : boxes) {
|
||||
if (weight != null) weight = box.getWeight() == null || box.getWeight().signum() <= 0 ? null : weight.add(box.getWeight());
|
||||
if (volume != null) volume = box.getLength() == null || box.getWidth() == null || box.getHeight() == null
|
||||
|| box.getLength().signum() <= 0 || box.getWidth().signum() <= 0 || box.getHeight().signum() <= 0 ? null
|
||||
: volume.add(box.getLength().multiply(box.getWidth()).multiply(box.getHeight()).movePointLeft(6));
|
||||
}
|
||||
String locationCode = locationCode(warehouseId, pallet.locationId());
|
||||
return new PalletOperationResponses.PalletContent(pallet.palletId(), pallet.palletCode(), pallet.status(),
|
||||
pallet.version(), pallet.locationId(), locationCode, destinations.size() == 1 ? destinations.iterator().next() : null,
|
||||
total, inventory);
|
||||
total, inventory, weight, volume);
|
||||
}
|
||||
|
||||
private PdaPalletOperation begin(PdaWarehouseIdentity identity, String type, String key, String requestHash) {
|
||||
|
||||
+12
-1
@@ -42,7 +42,18 @@ public final class PdaRelabelResponses {
|
||||
List<SkuItem> skuItems,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages, int cartonProcessedPackages, int skuProcessedPackages,
|
||||
String destinationFbaWarehouseCode, Long returnCartonId, long returnQuantity) {
|
||||
String destinationFbaWarehouseCode, Long returnCartonId, long returnQuantity,
|
||||
String sourcePalletCode, String sourcePalletBasis) {
|
||||
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, Long returnCartonId, long returnQuantity) {
|
||||
this(itemId, skuCode, productNameZh, shipmentNo, trackingCode, newSkuCode, labelRequirement,
|
||||
relabelFileId, relabelFileName, relabelFileUrl, cartonMark, fbaCartonMark, operationType, skuItems,
|
||||
expectedPackages, relabelledPackages, skippedPackages, exceptionPackages, cartonProcessedPackages,
|
||||
skuProcessedPackages, destinationFbaWarehouseCode, returnCartonId, returnQuantity, null, null);
|
||||
}
|
||||
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,
|
||||
|
||||
+12
-1
@@ -92,6 +92,7 @@ public class PdaRelabelService {
|
||||
@Resource private PdaRelabelTaskMapper tasks;
|
||||
@Resource private PdaRelabelUnitFactMapper unitFacts;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper sourcePositions;
|
||||
@Resource private PdaRelabelOperationMapper operations;
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private RelabelApplicationService applicationService;
|
||||
@@ -786,7 +787,7 @@ public class PdaRelabelService {
|
||||
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.getReturnCartonId(),
|
||||
returnQuantity(item));
|
||||
returnQuantity(item), sourcePalletCode(item), item.getReturnCartonId() != null ? "RETURN_CARTON" : "INVENTORY_REFERENCE");
|
||||
}).toList();
|
||||
List<PdaRelabelResponses.UnitFact> factResponses = facts.stream().map(fact ->
|
||||
new PdaRelabelResponses.UnitFact(fact.getId(), fact.getItemId(), fact.getOldLabel(), fact.getNewLabel(),
|
||||
@@ -881,6 +882,16 @@ public class PdaRelabelService {
|
||||
List.class, PdaRelabelResponses.SkuItem.class));
|
||||
} catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read relabel SKU details", exception); }
|
||||
}
|
||||
private String sourcePalletCode(RelabelItem item) {
|
||||
if (item.getReturnCartonId() != null) {
|
||||
var carton = returnCartons == null ? null : returnCartons.selectById(item.getReturnCartonId());
|
||||
return carton != null && Objects.equals(item.getCustomerId(), carton.getCustomerId()) ? carton.getPalletCode() : null;
|
||||
}
|
||||
var position = sourcePositions == null || item.getSourceInventoryPositionId() == null ? null
|
||||
: sourcePositions.findTenantPosition(item.getCustomerId(), item.getSourceInventoryPositionId());
|
||||
return position != null ? position.getPalletNo() : null;
|
||||
}
|
||||
|
||||
private long returnQuantity(RelabelItem item) {
|
||||
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
|
||||
return carton == null || carton.getTotalQuantity() == null ? 0L : carton.getTotalQuantity();
|
||||
|
||||
+9
-2
@@ -25,11 +25,18 @@ public final class PdaReturnReceiptRequests {
|
||||
@Size(max = 16) String unitType,
|
||||
@Size(max = 128) String unitCode,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
Long totalBoxCount) {
|
||||
Long totalBoxCount, Boolean originalPackaging) {
|
||||
public Confirm(String returnNo, String trackingNo, String startedBy, String palletCode,
|
||||
List<@Valid Item> items, String idempotencyKey) {
|
||||
this(returnNo, trackingNo, startedBy, palletCode, items, idempotencyKey,
|
||||
null, null, null, null, null, null, null);
|
||||
null, null, null, null, null, null, null, null);
|
||||
}
|
||||
public Confirm(String returnNo, String trackingNo, String startedBy, String palletCode,
|
||||
List<@Valid Item> items, String idempotencyKey, String unitType, String unitCode,
|
||||
BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight,
|
||||
Long totalBoxCount) {
|
||||
this(returnNo, trackingNo, startedBy, palletCode, items, idempotencyKey, unitType, unitCode,
|
||||
length, width, height, weight, totalBoxCount, null);
|
||||
}
|
||||
}
|
||||
public record Item(@NotBlank @Size(max = 128) String skuCode,
|
||||
|
||||
+8
-2
@@ -3,14 +3,20 @@ package com.baiyue.lastleg.returnreceipt.dto;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class ReturnCartonCountDtos {
|
||||
private ReturnCartonCountDtos() { }
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(50) long size, @Size(max=128) String keyword) { }
|
||||
public record Sku(String skuCode, String productNameZh, long quantity) { }
|
||||
public record Box(String cartonMark, List<Sku> items) { }
|
||||
public record Box(String cartonMark, List<Sku> items, BigDecimal length, BigDecimal width, BigDecimal height, BigDecimal weight) {
|
||||
public Box(String cartonMark, List<Sku> items) { this(cartonMark, items, null, null, null, null); }
|
||||
}
|
||||
public record Task(String receiptNo, String returnNo, String palletCode, String status, List<Sku> expectedItems, List<Box> boxes) { }
|
||||
public record CountItem(@NotBlank @Size(max=128) String skuCode, @Min(1) long quantity) { }
|
||||
public record CountBox(@NotBlank @Size(max=128) String cartonMark, @NotEmpty @Size(max=100) List<@NotNull @Valid CountItem> items) { }
|
||||
public record CountBox(@NotBlank @Size(max=128) String cartonMark, @NotEmpty @Size(max=100) List<@NotNull @Valid CountItem> items,
|
||||
@Positive BigDecimal length, @Positive BigDecimal width, @Positive BigDecimal height, @Positive BigDecimal weight) {
|
||||
public CountBox(String cartonMark, List<@NotNull @Valid CountItem> items) { this(cartonMark, items, null, null, null, null); }
|
||||
}
|
||||
public record Confirm(@NotBlank @Size(max=128) String idempotencyKey, @NotEmpty @Size(max=500) List<@NotNull @Valid CountBox> boxes) { }
|
||||
}
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ public class ReturnInventoryCarton {
|
||||
private BigDecimal width;
|
||||
private BigDecimal height;
|
||||
private BigDecimal weight;
|
||||
private Boolean originalPackaging;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime mergedAt;
|
||||
|
||||
+4
@@ -23,6 +23,10 @@ public class ReturnReceipt {
|
||||
private BigDecimal width;
|
||||
private BigDecimal height;
|
||||
private BigDecimal weight;
|
||||
/** Whether the signed physical unit reused its original box/packaging. */
|
||||
private Boolean originalPackaging;
|
||||
/** PALLET_MEASURED for a measured carrier, UNIT_SUM for child-unit aggregation. */
|
||||
private String measurementSource;
|
||||
private String palletCode;
|
||||
private String cartonMark;
|
||||
private String trackingNo;
|
||||
|
||||
+4
@@ -10,6 +10,10 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryCarton> {
|
||||
@Select("SELECT * FROM ll_return_inventory_carton WHERE overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND pallet_code=#{palletCode} AND total_quantity>0 " +
|
||||
"AND status IN ('AVAILABLE','PACKING_RESERVED','RELABEL_PENDING','OUTBOUND_PENDING','DISCARD_PENDING') ORDER BY id")
|
||||
List<ReturnInventoryCarton> palletCartons(@Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
@Select("SELECT EXISTS(SELECT 1 FROM ll_return_inventory_outbound_movement WHERE carton_id=#{cartonId} AND quantity>0)")
|
||||
boolean hasOutbound(@Param("cartonId") long cartonId);
|
||||
@Update("UPDATE ll_return_inventory_carton SET relabel_application_id=NULL,relabel_no=NULL,relabel_status=NULL WHERE id=#{id}")
|
||||
|
||||
+4
-2
@@ -129,6 +129,7 @@ public class ReturnCartonCountService {
|
||||
Set<String> marks = new HashSet<>();
|
||||
for (CountBox box : request.boxes()) {
|
||||
if (!byMark.containsKey(box.cartonMark()) || !marks.add(box.cartonMark())) throw error(HttpStatus.BAD_REQUEST, "箱号不属于本次签收或重复填写");
|
||||
if (box.length() == null || box.width() == null || box.height() == null || box.weight() == null || box.length().signum() <= 0 || box.width().signum() <= 0 || box.height().signum() <= 0 || box.weight().signum() <= 0) throw error(HttpStatus.BAD_REQUEST, "每个箱子必须填写有效的长、宽、高和重量");
|
||||
Set<String> skus = new HashSet<>();
|
||||
for (CountItem line : box.items()) {
|
||||
if (!expected.containsKey(line.skuCode()) || !skus.add(line.skuCode()) || line.quantity() <= 0) throw error(HttpStatus.BAD_REQUEST, "SKU 不属于本次签收、数量无效或同箱重复");
|
||||
@@ -144,7 +145,7 @@ public class ReturnCartonCountService {
|
||||
item.setCustomerId(box.getCustomerId()); item.setSkuCode(line.skuCode()); item.setProductNameZh(names.get(line.skuCode())); item.setQuantity(line.quantity());
|
||||
item.setPackingReservedQuantity(0L); items.insert(item);
|
||||
}
|
||||
box.setSkuTypes(input.items().size()); box.setTotalQuantity(input.items().stream().mapToLong(CountItem::quantity).sum());
|
||||
box.setSkuTypes(input.items().size()); box.setTotalQuantity(input.items().stream().mapToLong(CountItem::quantity).sum()); box.setLength(input.length()); box.setWidth(input.width()); box.setHeight(input.height()); box.setWeight(input.weight());
|
||||
box.setCountStatus("CONFIRMED"); box.setCountedAt(now); box.setCountedBy(identity.operatorKey()); box.setUpdatedAt(now); cartons.updateById(box);
|
||||
}
|
||||
for (String sku : expected.keySet()) {
|
||||
@@ -167,7 +168,8 @@ public class ReturnCartonCountService {
|
||||
private Task task(ReturnReceipt receipt, List<ReturnInventoryCarton> boxes) {
|
||||
List<Sku> expected = receiptItems.listReceipt(receipt.getId()).stream().map(line -> new Sku(line.getSkuCode(), null, line.getReceivedQuantity())).toList();
|
||||
List<Box> contents = boxes.stream().map(box -> new Box(box.getCartonMark(), items.listCarton(box.getId()).stream()
|
||||
.map(line -> new Sku(line.getSkuCode(), line.getProductNameZh(), "PENDING".equals(box.getCountStatus()) ? 0 : line.getQuantity())).toList())).toList();
|
||||
.map(line -> new Sku(line.getSkuCode(), line.getProductNameZh(), "PENDING".equals(box.getCountStatus()) ? 0 : line.getQuantity())).toList(),
|
||||
box.getLength(), box.getWidth(), box.getHeight(), box.getWeight())).toList();
|
||||
return new Task(receipt.getReceiptNo(), boxes.isEmpty() ? null : boxes.get(0).getSourceReturnNo(), receipt.getPalletCode(),
|
||||
boxes.stream().allMatch(box -> "CONFIRMED".equals(box.getCountStatus())) ? "CONFIRMED" : sessions.findReceipt(receipt.getId()) != null ? "COUNTING" : "PENDING", expected, contents);
|
||||
}
|
||||
|
||||
+11
@@ -509,6 +509,7 @@ public class ReturnInventoryService {
|
||||
// Pallet measurements describe the carrier, never each child carton.
|
||||
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.setOriginalPackaging(receipt.getOriginalPackaging());
|
||||
}
|
||||
carton.setStatus(AVAILABLE); carton.setSkuTypes(lines.size()); carton.setTotalQuantity(total); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
@@ -1086,6 +1087,16 @@ public class ReturnInventoryService {
|
||||
}
|
||||
if (existingTarget != null) {
|
||||
target.setTotalQuantity(cartonItems.totalQuantity(targetId)); target.setSkuTypes(cartonItems.skuTypes(targetId));
|
||||
if (request.weight() != null) target.setWeight(request.weight());
|
||||
else {
|
||||
BigDecimal combinedWeight = target.getWeight();
|
||||
for (var source : sources) combinedWeight = combinedWeight == null || source.getWeight() == null
|
||||
? null : combinedWeight.add(source.getWeight());
|
||||
target.setWeight(combinedWeight);
|
||||
}
|
||||
if (request.length() != null) target.setLength(request.length());
|
||||
if (request.width() != null) target.setWidth(request.width());
|
||||
if (request.height() != null) target.setHeight(request.height());
|
||||
target.setUpdatedAt(now); target.setLocationCode(null); target.setPutawayAt(null); cartons.updateById(target); cartons.clearPutaway(targetId);
|
||||
}
|
||||
for (TraceTransfer transfer : transfers) packingTrace.insert(IdWorker.getId(), customerId, operation.getId(),
|
||||
|
||||
+10
@@ -618,6 +618,14 @@ public class ReturnReceiptService {
|
||||
|| request.height().signum() <= 0 || request.weight().signum() <= 0)) {
|
||||
throw badRequest("托盘签收必须填写有效的长、宽、高和重量");
|
||||
}
|
||||
if ("BOX".equals(unitType) || "PARCEL".equals(unitType)) {
|
||||
if (request.originalPackaging() == null) throw badRequest("箱子或包裹必须选择是否使用原箱原包装");
|
||||
if (request.length() == null || request.width() == null || request.height() == null || request.weight() == null
|
||||
|| request.length().signum() <= 0 || request.width().signum() <= 0
|
||||
|| request.height().signum() <= 0 || request.weight().signum() <= 0) {
|
||||
throw badRequest("箱子或包裹必须填写有效的长、宽、高和重量");
|
||||
}
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long receiptId = IdWorker.getId();
|
||||
ReturnReceipt receipt = new ReturnReceipt();
|
||||
@@ -629,6 +637,8 @@ public class ReturnReceiptService {
|
||||
receipt.setTotalBoxCount(totalBoxCount);
|
||||
receipt.setPalletCode("PALLET".equals(unitType) ? unitCode : ("BOX".equals(unitType) ? text(request.palletCode()) : null));
|
||||
receipt.setLength(request.length()); receipt.setWidth(request.width()); receipt.setHeight(request.height()); receipt.setWeight(request.weight());
|
||||
receipt.setOriginalPackaging(request.originalPackaging());
|
||||
receipt.setMeasurementSource("PALLET".equals(unitType) ? "PALLET_MEASURED" : "UNIT_MEASURED");
|
||||
receipt.setReceivedQuantity(receiptQuantity); receipt.setOperatorId(identity.operatorKey());
|
||||
receipt.setOperatorName(identity.operatorName()); receipt.setIdempotencyKey(key); receipt.setRequestHash(requestHash);
|
||||
receipt.setSignedAt(now); receipt.setCreatedAt(now);
|
||||
|
||||
@@ -3018,6 +3018,8 @@ EXECUTE ll_return_receipt_alter_stmt;
|
||||
DEALLOCATE PREPARE ll_return_receipt_alter_stmt;
|
||||
SET @ll_return_receipt_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''本次签收绑定的托盘码'' AFTER receipt_batch_no'));
|
||||
PREPARE ll_return_receipt_add_pallet_stmt FROM @ll_return_receipt_add_pallet; EXECUTE ll_return_receipt_add_pallet_stmt; DEALLOCATE PREPARE ll_return_receipt_add_pallet_stmt;
|
||||
SET @ll_return_receipt_add_measurement_facts = (SELECT IF(COUNT(*)=0, 'ALTER TABLE ll_return_receipt ADD COLUMN original_packaging TINYINT(1) NULL COMMENT ''是否使用原箱原包装'', ADD COLUMN measurement_source VARCHAR(32) NULL COMMENT ''尺寸重量来源'' AFTER weight', 'SELECT 1') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='original_packaging');
|
||||
PREPARE ll_return_receipt_add_measurement_facts_stmt FROM @ll_return_receipt_add_measurement_facts; EXECUTE ll_return_receipt_add_measurement_facts_stmt; DEALLOCATE PREPARE ll_return_receipt_add_measurement_facts_stmt;
|
||||
CREATE TABLE IF NOT EXISTS ll_return_receipt_item (
|
||||
id BIGINT NOT NULL COMMENT '签收SKU明细ID', receipt_id BIGINT NOT NULL COMMENT 'PDA退货签收单ID', return_order_item_id BIGINT NOT NULL COMMENT '退货预报明细ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', received_quantity BIGINT NOT NULL COMMENT '本次SKU实收数量',
|
||||
PRIMARY KEY(id), KEY idx_ll_return_receipt_item_receipt(receipt_id), KEY idx_ll_return_receipt_item_order_item(return_order_item_id), CONSTRAINT chk_ll_return_receipt_item_quantity CHECK(received_quantity>0)
|
||||
@@ -3037,6 +3039,8 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_carton (
|
||||
) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛';
|
||||
SET @ll_return_carton_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''签收时绑定的托盘码'' AFTER source_batch_no'));
|
||||
PREPARE ll_return_carton_add_pallet_stmt FROM @ll_return_carton_add_pallet; EXECUTE ll_return_carton_add_pallet_stmt; DEALLOCATE PREPARE ll_return_carton_add_pallet_stmt;
|
||||
SET @ll_return_carton_add_original_packaging = (SELECT IF(COUNT(*)=0, 'ALTER TABLE ll_return_inventory_carton ADD COLUMN original_packaging TINYINT(1) NULL COMMENT ''是否使用原箱原包装'' AFTER weight', 'SELECT 1') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='original_packaging');
|
||||
PREPARE ll_return_carton_add_original_packaging_stmt FROM @ll_return_carton_add_original_packaging; EXECUTE ll_return_carton_add_original_packaging_stmt; DEALLOCATE PREPARE ll_return_carton_add_original_packaging_stmt;
|
||||
-- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 SQL 兼容 Spring 的数据库初始化器。
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
|
||||
PREPARE return_inventory_carton_check_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_stmt;
|
||||
|
||||
+78
@@ -92,6 +92,84 @@ class PdaPalletOperationServiceTest {
|
||||
service.configureTransactions(new NoOpTransactionManager());
|
||||
}
|
||||
|
||||
@Test void returnPalletTotalsFollowCurrentBindingsWithoutCountingRetriesTwice() {
|
||||
var first = new ReturnInventoryCarton(); first.setWeight(new java.math.BigDecimal("5"));
|
||||
first.setLength(new java.math.BigDecimal("30")); first.setWidth(new java.math.BigDecimal("20"));
|
||||
first.setHeight(new java.math.BigDecimal("20")); first.setDestinationWarehouseCode("SCK8");
|
||||
var second = new ReturnInventoryCarton(); second.setWeight(new java.math.BigDecimal("11"));
|
||||
second.setLength(new java.math.BigDecimal("40")); second.setWidth(new java.math.BigDecimal("30"));
|
||||
second.setHeight(new java.math.BigDecimal("30")); second.setDestinationWarehouseCode("SCK8");
|
||||
var pallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "OCCUPIED", "SCK8", 2);
|
||||
when(returnCartons.palletCartons("88", "TARGET")).thenReturn(List.of(first), List.of(first, second), List.of(first, second), List.of(first));
|
||||
var before = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.PalletContent) ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet);
|
||||
assertEquals(0, before.grossWeightKg().compareTo(new java.math.BigDecimal("5")));
|
||||
var added = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.PalletContent) ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet);
|
||||
assertEquals(2, added.totalPackages());
|
||||
assertEquals(0, added.grossWeightKg().compareTo(new java.math.BigDecimal("16")));
|
||||
assertEquals(0, added.volumeCbm().compareTo(new java.math.BigDecimal("0.048")));
|
||||
assertEquals(added, ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet));
|
||||
assertEquals(before, ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet));
|
||||
}
|
||||
|
||||
@Test void returnRebindingRequiresCurrentRoundBindingAndAddsToExistingPallet() {
|
||||
var workTasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
var workHistory = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkTasks", workTasks);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkHistory", workHistory);
|
||||
var app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100");
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(700L);
|
||||
when(workTasks.find("100", 600L)).thenReturn(task);
|
||||
var item = new RelabelItem(); item.setReturnCartonId(801L); item.setCartonMark("BOX-1");
|
||||
item.setDestinationFbaWarehouseCode("SCK8"); item.setNotifiedPackageCount(1);
|
||||
var box = new ReturnInventoryCarton(); box.setId(801L); box.setCustomerId("100");
|
||||
box.setPalletCode("SOURCE"); box.setRelabelApplicationId(600L); box.setStatus("RELABEL_PENDING");
|
||||
box.setTotalQuantity(10L); box.setWeight(new java.math.BigDecimal("11"));
|
||||
box.setLength(new java.math.BigDecimal("40")); box.setWidth(new java.math.BigDecimal("30"));
|
||||
box.setHeight(new java.math.BigDecimal("30")); box.setDestinationWarehouseCode("SCK8");
|
||||
var existing = new ReturnInventoryCarton(); existing.setWeight(new java.math.BigDecimal("5"));
|
||||
existing.setLength(new java.math.BigDecimal("30")); existing.setWidth(new java.math.BigDecimal("20"));
|
||||
existing.setHeight(new java.math.BigDecimal("20")); existing.setDestinationWarehouseCode("SCK8");
|
||||
var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L, "OCCUPIED", "SCK8", 1);
|
||||
var target = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, 1010L, "OCCUPIED", "SCK8", 2);
|
||||
when(returnCartons.lockId(801L)).thenReturn(box);
|
||||
when(returnCartons.palletCartons("88", "TARGET")).thenAnswer(call ->
|
||||
"TARGET".equals(box.getPalletCode()) ? List.of(existing, box) : List.of(existing));
|
||||
var request = new PalletOperationRequests.RelabelDisassemble("rebind-current", "RL-1",
|
||||
new PalletOperationRequests.PalletExpectation("SOURCE", 1),
|
||||
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("BOX-1"), "rebind");
|
||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
|
||||
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
|
||||
verifyNoInteractions(physical);
|
||||
var binding = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
|
||||
binding.setDetailJson("{\"round\":\"INITIAL\",\"cartonMarks\":[\"BOX-1\"]}");
|
||||
when(workHistory.selectList(any())).thenReturn(List.of(binding));
|
||||
task.setReviewedAt(LocalDateTime.of(2026, 10, 12, 12, 0));
|
||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
|
||||
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
|
||||
task.setReviewedAt(null);
|
||||
when(physical.resolve("SOURCE")).thenReturn(source);
|
||||
when(physical.resolve("TARGET")).thenReturn(target);
|
||||
var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation)
|
||||
ReflectionTestUtils.invokeMethod(service, "bindReturnRelabelPallet", identity(), app,
|
||||
List.of(item), source, target, request);
|
||||
assertEquals("TARGET", box.getPalletCode());
|
||||
assertEquals(0, result.pallets().get(0).totalPackages());
|
||||
assertEquals(2, result.pallets().get(1).totalPackages());
|
||||
assertEquals(0, result.pallets().get(1).grossWeightKg().compareTo(new java.math.BigDecimal("16")));
|
||||
assertEquals(0, result.pallets().get(1).volumeCbm().compareTo(new java.math.BigDecimal("0.048")));
|
||||
verify(returnCartons).updateById(box);
|
||||
verify(workHistory).insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class));
|
||||
}
|
||||
|
||||
@Test void missingCartonMeasurementDoesNotBecomeZeroWeight() {
|
||||
var box = new ReturnInventoryCarton(); box.setWeight(new java.math.BigDecimal("5"));
|
||||
when(returnCartons.palletCartons("88", "TARGET")).thenReturn(List.of(box));
|
||||
var pallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "OCCUPIED", "SCK8", 2);
|
||||
var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.PalletContent) ReflectionTestUtils.invokeMethod(service, "content", 88L, pallet);
|
||||
assertNull(result.volumeCbm());
|
||||
assertEquals(0, result.grossWeightKg().compareTo(new java.math.BigDecimal("5")));
|
||||
}
|
||||
|
||||
@Test void partialReturnRelabelKeepsUnlabelledRemainderOnOriginalPallet() {
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100"); app.setRelabelNo("RL-1");
|
||||
RelabelItem item = new RelabelItem(); item.setId(701L); item.setSkuCode("SKU-1");
|
||||
|
||||
+25
@@ -82,6 +82,31 @@ class PdaRelabelServiceTest {
|
||||
lenient().when(tasks.updateWithVersion(any(), anyInt())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailDistinguishesInventoryReferenceFromRecordedReturnPallet() {
|
||||
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
var positions = org.mockito.Mockito.mock(com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper.class);
|
||||
ReflectionTestUtils.setField(service, "sourcePositions", positions);
|
||||
item.setSourceInventoryPositionId(30L);
|
||||
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition();
|
||||
position.setPalletNo("PLT-REFERENCE");
|
||||
when(positions.findTenantPosition("100", 30L)).thenReturn(position);
|
||||
var ordinary = service.detail("7", 900L, "FBR-1").items().get(0);
|
||||
assertEquals("PLT-REFERENCE", ordinary.sourcePalletCode());
|
||||
assertEquals("INVENTORY_REFERENCE", ordinary.sourcePalletBasis());
|
||||
var cartons = org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper.class);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons);
|
||||
item.setReturnCartonId(40L);
|
||||
var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton();
|
||||
carton.setCustomerId("100"); carton.setPalletCode("PLT-RETURN"); carton.setTotalQuantity(20L);
|
||||
when(cartons.selectById(40L)).thenReturn(carton);
|
||||
var returned = service.detail("7", 900L, "FBR-1").items().get(0);
|
||||
assertEquals("PLT-RETURN", returned.sourcePalletCode());
|
||||
assertEquals("RETURN_CARTON", returned.sourcePalletBasis());
|
||||
carton.setCustomerId("OTHER");
|
||||
assertEquals(null, service.detail("7", 900L, "FBR-1").items().get(0).sourcePalletCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesUseCustomerNameOnlyFromMatchingCustomerOrder() {
|
||||
var orders = org.mockito.Mockito.mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);
|
||||
|
||||
+2
-2
@@ -454,7 +454,7 @@ class ReturnReceiptServiceTest {
|
||||
stubNewReceipt(order, List.of(line), "SP-SOURCE");
|
||||
var request = new PdaReturnReceiptRequests.Confirm("RT-1", null, "SKU", null,
|
||||
List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1)), "parcel-key",
|
||||
"PARCEL", null, null, null, null, null, null);
|
||||
"PARCEL", null, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, null, true);
|
||||
var result = service.confirm(identity, request);
|
||||
var capture = ArgumentCaptor.forClass(ReturnReceipt.class);
|
||||
verify(receipts).insert(capture.capture());
|
||||
@@ -611,6 +611,6 @@ class ReturnReceiptServiceTest {
|
||||
// These focused tests exercise receipt/idempotency behavior at the
|
||||
// carton level. Pallet receipts have a separate multi-carton contract.
|
||||
return new PdaReturnReceiptRequests.Confirm("RT-1", trackingNo, startedBy, palletCode, boxed, key,
|
||||
"BOX", palletCode, null, null, null, null, 1L);
|
||||
"BOX", palletCode, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, java.math.BigDecimal.ONE, 1L, true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user