贴标申请,仓储转运
This commit is contained in:
@@ -18,7 +18,10 @@ public final class RelabelResponses {
|
||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
|
||||
String reviewChannel, String reviewRemark, LocalDateTime createdAt) {}
|
||||
String reviewChannel, String reviewRemark, LocalDateTime createdAt,
|
||||
LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt, LocalDateTime outboundAt) {}
|
||||
public record Milestones(LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt,
|
||||
LocalDateTime outboundAt) {}
|
||||
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode,
|
||||
@@ -39,7 +42,8 @@ public final class RelabelResponses {
|
||||
long availablePackages, long version, List<InventoryPosition> positions,
|
||||
String sourceType, Long returnCartonId, String cartonMark,
|
||||
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) {
|
||||
public InventoryCandidate(Long inventoryBalanceId, Long inboundOrderId, String inboundOrderNo, String inboundCode,
|
||||
String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName,
|
||||
String destinationFbaWarehouseCode, String destinationFbaWarehouseName, String skuCode, String productNameZh,
|
||||
@@ -48,7 +52,7 @@ public final class RelabelResponses {
|
||||
this(inventoryBalanceId, inboundOrderId, inboundOrderNo, inboundCode, overseasWarehouseId,
|
||||
overseasWarehouseCode, overseasWarehouseName, destinationFbaWarehouseCode, destinationFbaWarehouseName,
|
||||
skuCode, productNameZh, availablePackages, version, positions, sourceType, returnCartonId, cartonMark,
|
||||
skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false);
|
||||
skuItems, relabelledPackages, null, relabelledPackages, 0, null, null, null, List.of(), false, List.of());
|
||||
}
|
||||
}
|
||||
public record HistoricalOrder(Long id, String orderNo, String sourceOrderNo, String outboundOrderNo,
|
||||
|
||||
+29
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.lastleg.relabel.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -9,6 +10,34 @@ import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication> {
|
||||
// Read existing facts so historical applications receive the same timestamps as new ones.
|
||||
// COMPLETED alone is insufficient: SKU-only work can complete without leaving the warehouse.
|
||||
@Select("""
|
||||
SELECT
|
||||
(SELECT MAX(h.created_at) FROM ll_relabel_status_history h
|
||||
WHERE h.customer_id=a.customer_id AND h.application_id=a.id
|
||||
AND h.from_status='PENDING_REVIEW' AND h.to_status IN ('PENDING_PROCESSING','REJECTED')) AS reviewed_at,
|
||||
CASE WHEN a.status IN ('CUSTOMER_CONFIRM_PENDING','READY_FOR_OUTBOUND','COMPLETED') THEN
|
||||
COALESCE((SELECT MAX(t.submitted_at) FROM ll_relabel_pda_task t
|
||||
WHERE t.customer_id=a.customer_id AND t.application_id=a.id),
|
||||
(SELECT MAX(h.created_at) FROM ll_relabel_status_history h
|
||||
WHERE h.customer_id=a.customer_id AND h.application_id=a.id
|
||||
AND h.to_status='CUSTOMER_CONFIRM_PENDING'))
|
||||
END AS relabel_completed_at,
|
||||
COALESCE((SELECT MAX(t.completed_at) FROM ll_outbound_pda_task t
|
||||
JOIN ll_outbound_order o ON o.id=t.outbound_order_id AND o.customer_id=t.customer_id
|
||||
WHERE o.customer_id=a.customer_id AND o.outbound_no=a.outbound_order_no
|
||||
AND o.status='OUTBOUNDED' AND t.status='COMPLETED'),
|
||||
(SELECT MAX(w.outbound_at) FROM ll_warehouse_transfer_order w
|
||||
WHERE w.customer_id=a.customer_id AND w.relabel_application_id=a.id AND w.relabel_no=a.relabel_no),
|
||||
(SELECT MAX(h.created_at) FROM ll_relabel_status_history h
|
||||
WHERE h.customer_id=a.customer_id AND h.application_id=a.id
|
||||
AND h.from_status='READY_FOR_OUTBOUND' AND h.to_status='COMPLETED')) AS outbound_at
|
||||
FROM ll_relabel_application a WHERE a.customer_id=#{customerId} AND a.id=#{applicationId}
|
||||
""")
|
||||
RelabelResponses.Milestones findMilestones(
|
||||
@Param("customerId") String customerId, @Param("applicationId") Long applicationId);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1")
|
||||
RelabelApplication findNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
|
||||
|
||||
+5
@@ -52,6 +52,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PdaRelabelService {
|
||||
@Resource private com.baiyue.lastleg.relabel.service.ReturnSkuConversionService skuConversions;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.ReturnRelabelPackingService returnPacking;
|
||||
|
||||
@Transactional
|
||||
@@ -500,6 +501,9 @@ public class PdaRelabelService {
|
||||
StringUtils.hasText(order.getWarehouseRemark()) ? order.getWarehouseRemark() + "\n" + note : note));
|
||||
}
|
||||
}
|
||||
skuConversions.apply(application, detailItems, factList(task), mixedRound(task), histories.selectList(new LambdaQueryWrapper<PdaRelabelHistory>()
|
||||
.eq(PdaRelabelHistory::getTaskId, task.getId()).eq(PdaRelabelHistory::getCustomerId, application.getCustomerId())
|
||||
.eq(PdaRelabelHistory::getAction, "SKU_SCAN")));
|
||||
String before = task.getStatus();
|
||||
task.setStatus(WAIT_REVIEW); task.setCompletionPhotoFileIds(write(photoIds));
|
||||
task.setCompletionRemark(trim(request.remark())); task.setSubmittedAt(LocalDateTime.now());
|
||||
@@ -549,6 +553,7 @@ public class PdaRelabelService {
|
||||
if (!WAIT_REVIEW.equals(task.getStatus())) throw conflict("Relabel task is not awaiting review");
|
||||
requireVersion(task, request.expectedVersion());
|
||||
// Preserve the previous round before freeing the active-label uniqueness constraint.
|
||||
skuConversions.reverse(application);
|
||||
List<PdaRelabelUnitFact> previousFacts = factList(task);
|
||||
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, AVAILABLE, operatorKey, operatorName,
|
||||
Map.of("facts", previousFacts, "photoFileIds", Objects.toString(task.getCompletionPhotoFileIds(), "[]"),
|
||||
|
||||
+29
-7
@@ -93,6 +93,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private InventoryPositionMapper inventoryPositions;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.ReturnSkuConversionService skuConversions;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper skuConversionRows;
|
||||
@Resource private ReturnOrderMapper returnOrders;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper returnPackingTraces;
|
||||
@Resource private RelabelDocumentFileGateway documentFiles;
|
||||
@@ -278,15 +280,17 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.filter(item -> value(item.getQuantity()) > 0)
|
||||
.map(item -> {
|
||||
long remaining = "AVAILABLE".equals(carton.getStatus())
|
||||
? value(item.getQuantity()) - value(item.getPackingReservedQuantity()) : 0;
|
||||
? remainingRelabelQuantity(carton.getId(), item) : 0;
|
||||
return new RelabelResponses.SkuItem(item.getSkuCode(), null,
|
||||
Math.toIntExact(Math.max(remaining, 0)), null);
|
||||
}).toList();
|
||||
}).filter(item -> !"AVAILABLE".equals(carton.getStatus()) || item.quantity() > 0).toList();
|
||||
if (cartonSkus.isEmpty() && !java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus())) continue;
|
||||
String skuCodes = cartonSkus.stream().map(RelabelResponses.SkuItem::skuCode)
|
||||
.collect(Collectors.joining(","));
|
||||
long availablePackages = cartonSkus.stream().mapToLong(RelabelResponses.SkuItem::quantity).sum();
|
||||
long relabelledPackages = returnCartons.totalRelabelledQuantity(carton.getId());
|
||||
long relabelledPackages = Math.max(returnCartons.totalRelabelledQuantity(carton.getId()),
|
||||
cartonLines.stream().mapToLong(line -> value(line.getRelabelledQuantity())
|
||||
+ returnCartons.completedSkuQuantity(carton.getId(), line.getSkuCode())).sum());
|
||||
long reserved = returnCartons.relabelReservedQuantity(carton.getId());
|
||||
result.add(new RelabelResponses.InventoryCandidate(null, null, carton.getSourceReturnNo(),
|
||||
null, carton.getOverseasWarehouseId(), null, null, carton.getDestinationWarehouseCode(),
|
||||
@@ -295,7 +299,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
com.baiyue.lastleg.returnreceipt.service.ReturnRelabelProgress.status(value(carton.getTotalQuantity()), relabelledPackages, reserved), relabelledPackages, reserved, java.util.Set.of("MERGED", "OUTBOUNDED").contains(carton.getStatus()) ? 0L : value(carton.getTotalQuantity()), carton.getStatus(), carton.getRelabelNo(),
|
||||
returnPackingTraces.listCartonHistory(customerId, carton.getCartonMark()).stream()
|
||||
.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()
|
||||
.map(com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses.SkuConversion::from).toList()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -408,14 +414,18 @@ 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())), Long::sum));
|
||||
line -> remainingRelabelQuantity(sourceReturnCartonId, line), Long::sum));
|
||||
if (availableSku.values().stream().noneMatch(quantity -> quantity > 0))
|
||||
throw conflict("来源箱 SKU 已全部换标,没有剩余可换标数量");
|
||||
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))
|
||||
throw conflict("SKU " + skuItem.skuCode() + " 的换标数量超过来源箱可用数量");
|
||||
}
|
||||
if ("CARTON_AND_SKU".equals(operationType(item)) && !requestedSku.equals(availableSku))
|
||||
if ("CARTON_AND_SKU".equals(operationType(item)) && (!requestedSku.equals(availableSku)
|
||||
|| returnCartonItems.lockCarton(sourceReturnCartonId).stream()
|
||||
.anyMatch(line -> remainingRelabelQuantity(sourceReturnCartonId, line) < value(line.getQuantity()))))
|
||||
throw conflict("包裹/箱混贴必须包含实物全部 SKU 和件数;部分数量请先隔离为独立新箱");
|
||||
if (returnCartonItems.lockCarton(carton.getId()).stream().anyMatch(line -> value(line.getPackingReservedQuantity()) > 0))
|
||||
throw conflict("退货箱SKU已被合箱计划锁定");
|
||||
@@ -906,6 +916,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
throw conflict("Upload the shipping label before confirming relabel completion");
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
skuConversions.confirm(application);
|
||||
WarehouseTransferOrder linkedTransfer = warehouseTransfers == null ? null
|
||||
: warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
boolean existingStockTransfer = linkedTransfer != null
|
||||
@@ -1175,6 +1186,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) {
|
||||
LastlegOrder order = orders.selectById(value.getLastlegOrderId());
|
||||
RelabelResponses.Milestones milestones = applications.findMilestones(value.getCustomerId(), value.getId());
|
||||
String skuCodes = detailItems.stream().map(RelabelItem::getSkuCode).filter(Objects::nonNull).distinct().collect(Collectors.joining(","));
|
||||
return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(),
|
||||
order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(),
|
||||
@@ -1192,7 +1204,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(),
|
||||
order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER",
|
||||
null,
|
||||
value.getCreatedAt());
|
||||
value.getCreatedAt(), milestones == null ? null : milestones.reviewedAt(),
|
||||
milestones == null ? null : milestones.relabelCompletedAt(),
|
||||
milestones == null ? null : milestones.outboundAt());
|
||||
}
|
||||
|
||||
@Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); }
|
||||
@@ -1290,6 +1304,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private record SourceInventory(RelabelRequests.Item request, InventoryBalance balance,
|
||||
ReturnInventoryCarton returnCarton) {}
|
||||
private long remainingRelabelQuantity(long cartonId, ReturnInventoryCartonItem line) {
|
||||
return Math.max(0, value(line.getQuantity()) - value(line.getPackingReservedQuantity())
|
||||
- value(line.getRelabelledQuantity())
|
||||
- returnCartons.completedSkuQuantity(cartonId, line.getSkuCode()));
|
||||
}
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static int intValue(Integer value) { return value == null ? 0 : value; }
|
||||
private static boolean containsIgnoreCase(String value, String fragment) {
|
||||
@@ -1364,6 +1383,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
&& (item.skuItems() == null || item.skuItems().isEmpty())) {
|
||||
throw badRequest("SKU label changes require at least one SKU detail");
|
||||
}
|
||||
if (item.returnCartonId() != null && item.skuItems() != null && item.skuItems().stream()
|
||||
.anyMatch(sku -> sku.skuCode().trim().equalsIgnoreCase(sku.newSkuCode().trim())))
|
||||
throw badRequest("退货 SKU 转换的新旧编码必须不同");
|
||||
}
|
||||
private static String operationType(RelabelRequests.Item item) {
|
||||
return StringUtils.hasText(item.operationType()) ? item.operationType().trim().toUpperCase() : "CARTON_LABEL";
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.*;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** Converts physical contents and SKU balances together; the carton itself never gains or loses pieces. */
|
||||
@Service
|
||||
public class ReturnSkuConversionService {
|
||||
@Resource private ReturnSkuConversionMapper conversions;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper contents;
|
||||
@Resource private ReturnInventoryBalanceMapper balances;
|
||||
@Resource private ReturnPackingTraceMapper traces;
|
||||
@Resource private ReturnInventoryOperationMapper operations;
|
||||
@Resource private ObjectMapper json;
|
||||
|
||||
@Transactional
|
||||
public void apply(RelabelApplication app, List<RelabelItem> items, List<PdaRelabelUnitFact> facts, String round) {
|
||||
apply(app, items, facts, round, List.of());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void apply(RelabelApplication app, List<RelabelItem> items, List<PdaRelabelUnitFact> facts, String round,
|
||||
List<PdaRelabelHistory> scans) {
|
||||
var existing = new ArrayList<>(conversions.lockApplication(app.getCustomerId(), app.getId()));
|
||||
for (var item : items.stream().sorted(Comparator.comparing(RelabelItem::getReturnCartonId,
|
||||
Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(RelabelItem::getId)).toList()) {
|
||||
if (item.getReturnCartonId() == null || !Set.of("SKU_LABEL", "RETURN_BULK_SKU_LABEL", "CARTON_AND_SKU").contains(item.getOperationType())) continue;
|
||||
long completed = facts.stream().filter(f -> item.getId().equals(f.getItemId()) && "RELABELLED".equals(f.getResult()))
|
||||
.mapToLong(f -> f.getQuantity() == null ? 1 : f.getQuantity()).sum();
|
||||
if (completed == 0 && scans.isEmpty()) continue;
|
||||
var carton = ownedCarton(app, item.getReturnCartonId());
|
||||
try {
|
||||
if ("RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) {
|
||||
// 按件贴标未声明 SKU 编码变更时只保留作业事实,不生成不存在的新 SKU。
|
||||
if (completed == 0 || item.getNewSkuCode() == null || item.getNewSkuCode().isBlank()) continue;
|
||||
convert(app, item, carton, item.getSkuCode(), item.getNewSkuCode(), completed, round, existing);
|
||||
} else {
|
||||
var approved = json.readTree(item.getSkuRelabelItemsJson());
|
||||
var doneIndices = new HashSet<Integer>();
|
||||
if (completed == 0) for (var scan : scans) {
|
||||
if (!"SKU_SCAN".equals(scan.getAction())) continue;
|
||||
var detail = json.readTree(scan.getDetailJson());
|
||||
if (!round.equals(detail.path("round").asText()) || !item.getId().toString().equals(detail.path("itemId").asText())) continue;
|
||||
int index = detail.path("skuIndex").asInt(-1);
|
||||
if (index < 0 || index >= approved.size()) throw conflict("SKU 扫描明细索引无效");
|
||||
var sku = approved.get(index);
|
||||
if (!sku.path("skuCode").asText().equals(detail.path("oldSku").asText())
|
||||
|| !sku.path("newSkuCode").asText().equals(detail.path("newSku").asText())
|
||||
|| sku.path("quantity").asLong() != detail.path("quantity").asLong()) throw conflict("SKU 扫描记录与批准明细不一致");
|
||||
doneIndices.add(index);
|
||||
}
|
||||
for (int index = 0; index < approved.size(); index++) {
|
||||
if (completed == 0 && !doneIndices.contains(index)) continue;
|
||||
var sku = approved.get(index);
|
||||
convert(app, item, carton, sku.path("skuCode").asText(), sku.path("newSkuCode").asText(),
|
||||
Math.multiplyExact(completed == 0 ? 1 : completed, sku.path("quantity").asLong()), round, existing);
|
||||
}
|
||||
}
|
||||
} catch (java.io.IOException failure) { throw conflict("换标 SKU 明细无法读取"); }
|
||||
refreshTypes(carton);
|
||||
}
|
||||
}
|
||||
|
||||
private void convert(RelabelApplication app, RelabelItem item, ReturnInventoryCarton carton,
|
||||
String oldSku, String newSku, long quantity, String round, List<ReturnSkuConversion> existing) {
|
||||
oldSku = code(oldSku); newSku = code(newSku);
|
||||
if (quantity <= 0 || oldSku.equalsIgnoreCase(newSku)) throw conflict("新旧 SKU 必须不同且换标数量必须大于零");
|
||||
for (var row : existing) if (item.getId().equals(row.getItemId()) && round.equals(row.getRoundKey())
|
||||
&& oldSku.equalsIgnoreCase(row.getOldSkuCode()) && newSku.equalsIgnoreCase(row.getNewSkuCode())) {
|
||||
if (row.getQuantity() != quantity || "REVERSED".equals(row.getStatus())) throw conflict("本轮换标库存转换已变化");
|
||||
return;
|
||||
}
|
||||
var source = contents.lockSku(carton.getId(), oldSku);
|
||||
if (source == null || n(source.getQuantity()) - n(source.getPackingReservedQuantity()) - n(source.getRelabelledQuantity()) < quantity)
|
||||
throw conflict("旧 SKU 的未换标库存不足:" + oldSku);
|
||||
var target = contents.lockSku(carton.getId(), newSku);
|
||||
boolean removed = source.getQuantity() == quantity, added = target == null;
|
||||
var ownership = ownership(carton, oldSku, source.getQuantity());
|
||||
String product = source.getProductNameZh();
|
||||
source.setQuantity(source.getQuantity() - quantity);
|
||||
if (removed) contents.deleteById(source.getId()); else contents.updateById(source);
|
||||
if (target == null) {
|
||||
target = new ReturnInventoryCartonItem(); target.setId(IdWorker.getId()); target.setCartonId(carton.getId());
|
||||
target.setCustomerId(app.getCustomerId()); target.setSkuCode(newSku); target.setProductNameZh(product);
|
||||
target.setQuantity(quantity); target.setPackingReservedQuantity(0L); target.setRelabelledQuantity(quantity);
|
||||
contents.insert(target);
|
||||
} else {
|
||||
target.setQuantity(Math.addExact(n(target.getQuantity()), quantity));
|
||||
target.setRelabelledQuantity(Math.addExact(n(target.getRelabelledQuantity()), quantity)); contents.updateById(target);
|
||||
}
|
||||
adjustBalances(carton, oldSku, newSku, product, quantity, removed, added);
|
||||
var row = new ReturnSkuConversion(); row.setId(IdWorker.getId()); row.setCustomerId(app.getCustomerId());
|
||||
row.setOverseasWarehouseId(carton.getOverseasWarehouseId()); row.setApplicationId(app.getId()); row.setRelabelNo(app.getRelabelNo());
|
||||
row.setItemId(item.getId()); row.setRoundKey(round); row.setCartonId(carton.getId()); row.setCartonMark(carton.getCartonMark());
|
||||
row.setOldSkuCode(oldSku); row.setNewSkuCode(newSku); row.setProductNameZh(product); row.setQuantity(quantity);
|
||||
row.setStatus("APPLIED"); row.setCompletedAt(LocalDateTime.now());
|
||||
var movedOwners = new ArrayList<ReturnPackingTraceMapper.Ownership>();
|
||||
long remaining = quantity;
|
||||
for (var owner : ownership) {
|
||||
long moved = Math.min(remaining, owner.quantity());
|
||||
if (moved > 0) movedOwners.add(new ReturnPackingTraceMapper.Ownership(owner.returnNo(), moved));
|
||||
remaining -= moved;
|
||||
}
|
||||
if (remaining != 0) throw conflict("SKU 换标来源追溯数量不足");
|
||||
try { row.setOwnershipJson(json.writeValueAsString(movedOwners)); }
|
||||
catch (java.io.IOException failure) { throw new IllegalStateException("Cannot save SKU conversion ownership", failure); }
|
||||
conversions.insert(row); existing.add(row); traceConversion(row, movedOwners, false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void confirm(RelabelApplication app) {
|
||||
for (var row : conversions.lockApplication(app.getCustomerId(), app.getId())) if ("APPLIED".equals(row.getStatus())) {
|
||||
row.setStatus("CONFIRMED"); row.setConfirmedAt(LocalDateTime.now()); conversions.updateById(row);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void reverse(RelabelApplication app) {
|
||||
var rows = new ArrayList<>(conversions.lockApplication(app.getCustomerId(), app.getId())); Collections.reverse(rows);
|
||||
for (var row : rows) {
|
||||
if ("CONFIRMED".equals(row.getStatus())) throw conflict("已确认的 SKU 库存转换不能退回");
|
||||
if (!"APPLIED".equals(row.getStatus())) continue;
|
||||
var carton = ownedCarton(app, row.getCartonId());
|
||||
var source = contents.lockSku(carton.getId(), row.getNewSkuCode());
|
||||
long quantity = row.getQuantity();
|
||||
if (source == null || n(source.getQuantity()) < quantity || n(source.getRelabelledQuantity()) < quantity
|
||||
|| n(source.getPackingReservedQuantity()) > 0) throw conflict("已换标 SKU 库存已变化,不能退回");
|
||||
List<ReturnPackingTraceMapper.Ownership> ownership;
|
||||
try { ownership = json.readValue(row.getOwnershipJson(), new com.fasterxml.jackson.core.type.TypeReference<>() {}); }
|
||||
catch (java.io.IOException failure) { throw conflict("换标来源追溯记录无法读取"); }
|
||||
var target = contents.lockSku(carton.getId(), row.getOldSkuCode());
|
||||
boolean removed = source.getQuantity() == quantity, added = target == null;
|
||||
source.setQuantity(source.getQuantity() - quantity); source.setRelabelledQuantity(source.getRelabelledQuantity() - quantity);
|
||||
if (removed) contents.deleteById(source.getId()); else contents.updateById(source);
|
||||
if (target == null) {
|
||||
target = new ReturnInventoryCartonItem(); target.setId(IdWorker.getId()); target.setCartonId(carton.getId());
|
||||
target.setCustomerId(app.getCustomerId()); target.setSkuCode(row.getOldSkuCode()); target.setProductNameZh(row.getProductNameZh());
|
||||
target.setQuantity(quantity); target.setPackingReservedQuantity(0L); target.setRelabelledQuantity(0L); contents.insert(target);
|
||||
} else { target.setQuantity(Math.addExact(n(target.getQuantity()), quantity)); contents.updateById(target); }
|
||||
adjustBalances(carton, row.getNewSkuCode(), row.getOldSkuCode(), row.getProductNameZh(), quantity, removed, added);
|
||||
traceConversion(row, ownership, true);
|
||||
row.setStatus("REVERSED"); row.setReversedAt(LocalDateTime.now()); conversions.updateById(row); refreshTypes(carton);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustBalances(ReturnInventoryCarton carton, String oldSku, String newSku, String product,
|
||||
long quantity, boolean removed, boolean added) {
|
||||
boolean box = !"PARCEL".equals(carton.getSourceUnitType());
|
||||
if (balances.deductConversion(carton.getCustomerId(), carton.getOverseasWarehouseId(), oldSku, quantity, box && removed ? 1 : 0) != 1)
|
||||
throw conflict("退货 SKU 余额不足:" + oldSku);
|
||||
if (balances.adjust(IdWorker.getId(), carton.getCustomerId(), carton.getOverseasWarehouseId(), newSku,
|
||||
product, quantity, quantity, box && added ? 1 : 0) <= 0) throw conflict("新 SKU 库存入账失败");
|
||||
}
|
||||
|
||||
private ReturnInventoryCarton ownedCarton(RelabelApplication app, long id) {
|
||||
var carton = cartons.lockId(id);
|
||||
if (carton == null || !app.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !app.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|
||||
|| !app.getId().equals(carton.getRelabelApplicationId()) || !"RELABEL_PENDING".equals(carton.getStatus()))
|
||||
throw conflict("换标来源箱或包裹已变化");
|
||||
return carton;
|
||||
}
|
||||
|
||||
private void refreshTypes(ReturnInventoryCarton carton) {
|
||||
var lines = contents.lockCarton(carton.getId());
|
||||
if (lines.stream().mapToLong(i -> n(i.getQuantity())).sum() != n(carton.getTotalQuantity())) throw conflict("SKU 转换后箱内件数不守恒");
|
||||
carton.setSkuTypes(lines.size()); carton.setUpdatedAt(LocalDateTime.now()); cartons.updateById(carton);
|
||||
}
|
||||
|
||||
private List<ReturnPackingTraceMapper.Ownership> ownership(ReturnInventoryCarton carton, String sku, long quantity) {
|
||||
var result = new ArrayList<>(traces.ownership(carton.getCustomerId(), carton.getCartonMark(), sku));
|
||||
long traced = result.stream().mapToLong(ReturnPackingTraceMapper.Ownership::quantity).sum();
|
||||
if (traced < quantity) {
|
||||
String[] sources = code(carton.getSourceReturnNo()).split(",");
|
||||
if (sources.length != 1) throw conflict("混箱 SKU 缺少来源追溯,不能转换库存");
|
||||
result.add(new ReturnPackingTraceMapper.Ownership(sources[0], quantity - traced));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void traceConversion(ReturnSkuConversion row, List<ReturnPackingTraceMapper.Ownership> ownership, boolean reverse) {
|
||||
var op = new ReturnInventoryOperation(); op.setId(IdWorker.getId()); op.setOperatorId("SKU_CONVERSION:" + row.getCustomerId());
|
||||
op.setIdempotencyKey(row.getId() + (reverse ? ":REVERSE" : ":APPLY")); op.setRequestHash(op.getIdempotencyKey());
|
||||
op.setOperationType(reverse ? "SKU_CONVERSION_REVERSE" : "SKU_CONVERSION"); op.setResultCartonId(row.getCartonId());
|
||||
op.setResultJson("{}"); op.setCreatedAt(LocalDateTime.now()); op.setUpdatedAt(op.getCreatedAt()); operations.insert(op);
|
||||
long remaining = row.getQuantity();
|
||||
for (var owner : ownership) {
|
||||
long moved = Math.min(remaining, owner.quantity()); if (moved == 0) continue;
|
||||
traces.insert(IdWorker.getId(), row.getCustomerId(), op.getId(), row.getCartonMark(), "", owner.returnNo(),
|
||||
reverse ? row.getNewSkuCode() : row.getOldSkuCode(), moved, op.getCreatedAt());
|
||||
traces.insert(IdWorker.getId(), row.getCustomerId(), op.getId(), "", row.getCartonMark(), owner.returnNo(),
|
||||
reverse ? row.getOldSkuCode() : row.getNewSkuCode(), moved, op.getCreatedAt());
|
||||
remaining -= moved;
|
||||
}
|
||||
if (remaining != 0) throw conflict("SKU 换标来源追溯数量不足");
|
||||
}
|
||||
private static long n(Long value) { return value == null ? 0 : value; }
|
||||
private static String code(String value) {
|
||||
if (value == null || value.isBlank()) throw conflict("SKU 或来源单号缺失"); return value.trim();
|
||||
}
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
+17
-5
@@ -17,18 +17,30 @@ public final class ReturnInventoryResponses {
|
||||
|
||||
public record InventoryItem(String overseasWarehouseId, String cartonMark, String sourceBatchNo, String status, String skuCode,
|
||||
String productNameZh, long quantity, int cartonSkuTypes, long cartonTotalQuantity,
|
||||
LocalDateTime receivedAt) { }
|
||||
LocalDateTime receivedAt, long relabelledQuantity, long remainingRelabelQuantity) { }
|
||||
public record SummaryItem(String overseasWarehouseId, String skuCode, String productNameZh,
|
||||
long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt) { }
|
||||
long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt,
|
||||
long relabelLockedQuantity) { }
|
||||
public record CartonDetail(String cartonMark, String customerId, String overseasWarehouseId, String sourceBatchNo, String sourceReturnNo, String status,
|
||||
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,
|
||||
long relabelledQuantity, long relabelReservedQuantity) { }
|
||||
long relabelledQuantity, long relabelReservedQuantity, List<SkuConversion> skuConversions) { }
|
||||
public record SkuConversion(String relabelNo, String cartonMark, String oldSkuCode, String newSkuCode,
|
||||
long quantity, String status, LocalDateTime completedAt) {
|
||||
public static SkuConversion from(com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion row) {
|
||||
return new SkuConversion(row.getRelabelNo(), row.getCartonMark(), row.getOldSkuCode(), row.getNewSkuCode(),
|
||||
row.getQuantity(), row.getStatus(), row.getCompletedAt());
|
||||
}
|
||||
}
|
||||
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) {
|
||||
public record CartonItem(String skuCode, String productNameZh, long quantity, long availableQuantity,
|
||||
long relabelledQuantity, long remainingRelabelQuantity) {
|
||||
public CartonItem(String skuCode, String productNameZh, long quantity, long availableQuantity) {
|
||||
this(skuCode, productNameZh, quantity, availableQuantity, 0, availableQuantity);
|
||||
}
|
||||
public CartonItem(String skuCode, String productNameZh, long quantity) { this(skuCode, productNameZh, quantity, quantity); }
|
||||
}
|
||||
public record Label(String cartonMark, String overseasWarehouseId, String customerId, String sourceBatchNo, String sourceReturnNo,
|
||||
@@ -54,7 +66,7 @@ public final class ReturnInventoryResponses {
|
||||
public record PackingPlanCompletion(String planNo, List<Label> labels) { }
|
||||
public record PlatformStock(String customerId, String overseasWarehouseId, String skuCode, String productNameZh,
|
||||
long totalQuantity, long availableQuantity, long cartonQuantity, LocalDateTime updatedAt,
|
||||
long looseAvailableQuantity) { }
|
||||
long looseAvailableQuantity, long relabelLockedQuantity) { }
|
||||
public record PlatformCarton(String customerId, String overseasWarehouseId, String cartonMark, String sourceReturnNo,
|
||||
String sourceUnitType, String sourceBatchNo, String status, int skuTypes, long totalQuantity,
|
||||
String fbaCartonMark, String destinationWarehouseCode, String destinationWarehouseName, String relabelNo, String relabelStatus,
|
||||
|
||||
+4
-1
@@ -16,9 +16,12 @@ public class ReturnInventoryBalance {
|
||||
private String overseasWarehouseId;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
/** 实物总库存;只有实际出库才会减少。 */
|
||||
/** 当前 SKU 的实物总库存;实际出库及 SKU 转换会减少,SKU 转换前后全仓件数守恒。 */
|
||||
private Long totalQuantity;
|
||||
private Long availableQuantity;
|
||||
/** 因换标整箱锁定的 SKU 件数,仅查询投影,不写入余额表。 */
|
||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
||||
private Long relabelLockedQuantity;
|
||||
private Long cartonQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
+2
@@ -15,4 +15,6 @@ public class ReturnInventoryCartonItem {
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
private Long packingReservedQuantity;
|
||||
/** 当前 SKU 中已经换过标的实物件数,不再允许二次换标。 */
|
||||
private Long relabelledQuantity;
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.returnreceipt.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_return_sku_conversion")
|
||||
public class ReturnSkuConversion {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String overseasWarehouseId;
|
||||
private Long applicationId;
|
||||
private String relabelNo;
|
||||
private Long itemId;
|
||||
private String roundKey;
|
||||
private Long cartonId;
|
||||
private String cartonMark;
|
||||
private String oldSkuCode;
|
||||
private String newSkuCode;
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
private String status;
|
||||
private String ownershipJson;
|
||||
private LocalDateTime completedAt;
|
||||
private LocalDateTime confirmedAt;
|
||||
private LocalDateTime reversedAt;
|
||||
}
|
||||
+10
-1
@@ -12,6 +12,11 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
|
||||
@Update("UPDATE ll_return_inventory_balance SET total_quantity=total_quantity-#{quantity},available_quantity=available_quantity-#{quantity}," +
|
||||
"carton_quantity=carton_quantity-#{cartons},updated_at=NOW(3) WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND sku_code=#{skuCode} AND total_quantity>=#{quantity} AND available_quantity>=#{quantity} AND carton_quantity>=#{cartons}")
|
||||
int deductConversion(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
@Param("skuCode") String skuCode, @Param("quantity") long quantity, @Param("cartons") long cartons);
|
||||
@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} " +
|
||||
@@ -24,7 +29,11 @@ public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventory
|
||||
"LEAST(b.available_quantity,COALESCE((SELECT SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))) " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " +
|
||||
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id AND i.sku_code=b.sku_code " +
|
||||
"AND c.status='AVAILABLE' AND c.count_status='CONFIRMED'),0)) AS available_quantity";
|
||||
"AND c.status='AVAILABLE' AND c.count_status='CONFIRMED'),0)) AS available_quantity," +
|
||||
"COALESCE((SELECT SUM(i.quantity) FROM ll_return_inventory_carton c " +
|
||||
"JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " +
|
||||
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id " +
|
||||
"AND i.sku_code=b.sku_code AND c.status='RELABEL_PENDING'),0) AS relabel_locked_quantity";
|
||||
@Select("SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} FOR UPDATE")
|
||||
ReturnInventoryBalance lockSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
|
||||
+15
-2
@@ -75,12 +75,25 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
|
||||
"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 " +
|
||||
String ALL_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 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 + ")";
|
||||
"JSON_QUOTE(CAST(ri.id AS CHAR)),'$.sourceItemIds') AND ra.status NOT IN ('CANCELLED','REJECTED') ";
|
||||
String COMPLETION_DEDUCTION = ALL_COMPLETION_DEDUCTION + CURRENT_RELABEL_ROUND + ")";
|
||||
|
||||
// All rounds count here: selecting a later partial application must not erase earlier completed pieces.
|
||||
@Select("SELECT GREATEST(0,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) - " +
|
||||
ALL_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') AND NOT EXISTS(SELECT 1 FROM ll_return_sku_conversion sc " +
|
||||
"WHERE sc.item_id=ri.id AND sc.status IN ('APPLIED','CONFIRMED') AND UPPER(sc.old_sku_code)=UPPER(#{skuCode}))")
|
||||
long completedSkuQuantity(@Param("cartonId") long cartonId, @Param("skuCode") String skuCode);
|
||||
@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) - " +
|
||||
|
||||
+5
-1
@@ -17,7 +17,11 @@ public interface ReturnInventoryMovementMapper extends com.baomidou.mybatisplus.
|
||||
"UNION ALL SELECT c.customer_id,c.overseas_warehouse_id,i.sku_code,i.product_name_zh,'MERGE',c.carton_mark,0,-i.quantity,c.created_at " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.source_batch_no='MERGED'" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,sku_code,product_name_zh,'OUTBOUND',outbound_no," +
|
||||
"-quantity,-quantity,occurred_at FROM ll_return_inventory_outbound_movement";
|
||||
"-quantity,-quantity,occurred_at FROM ll_return_inventory_outbound_movement" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,old_sku_code,product_name_zh,'SKU_CONVERSION_OUT',relabel_no,-quantity,-quantity,completed_at FROM ll_return_sku_conversion" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_IN',relabel_no,quantity,quantity,completed_at FROM ll_return_sku_conversion" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,old_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_IN',relabel_no,quantity,quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_OUT',relabel_no,-quantity,-quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'";
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM (" + MOVEMENTS + ") m WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND m.customerId=#{customerId} </if>" +
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ public interface ReturnInventoryQueryMapper {
|
||||
@Param("status") String status, @Param("returnNo") String returnNo);
|
||||
|
||||
@Select("<script>SELECT c.id carton_id,c.overseas_warehouse_id,c.carton_mark,c.source_batch_no,(CASE WHEN c.count_status='PENDING' AND c.status='AVAILABLE' THEN 'PENDING_COUNT' ELSE c.status END) AS status,c.sku_types,c.total_quantity,c.created_at, " +
|
||||
"i.sku_code,i.product_name_zh,i.quantity FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id " +
|
||||
"i.sku_code,i.product_name_zh,i.quantity,i.relabelled_quantity,i.packing_reserved_quantity FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id " +
|
||||
"WHERE c.customer_id=#{customerId} " +
|
||||
"<if test='status != null and status != \"\"'>AND (CASE WHEN c.count_status='PENDING' AND c.status='AVAILABLE' THEN 'PENDING_COUNT' ELSE c.status END)=#{status} </if>" +
|
||||
"<if test='returnNo != null and returnNo != \"\"'>AND (c.source_return_no LIKE CONCAT('%',#{returnNo},'%') OR EXISTS (SELECT 1 FROM ll_return_receipt r JOIN ll_return_order o ON o.id=r.return_order_id WHERE r.id=c.source_receipt_id AND o.return_no LIKE CONCAT('%',#{returnNo},'%'))) </if>" +
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnSkuConversionMapper extends BaseMapper<ReturnSkuConversion> {
|
||||
@Select("SELECT * FROM ll_return_sku_conversion WHERE customer_id=#{customerId} AND application_id=#{applicationId} ORDER BY id FOR UPDATE")
|
||||
List<ReturnSkuConversion> lockApplication(@Param("customerId") String customerId, @Param("applicationId") long applicationId);
|
||||
|
||||
@Select("SELECT sc.* FROM ll_return_sku_conversion sc WHERE sc.customer_id=#{customerId} AND (sc.carton_id=#{cartonId} " +
|
||||
"OR EXISTS(SELECT 1 FROM ll_relabel_item ri WHERE ri.id=sc.item_id AND ri.return_carton_id=#{cartonId})) ORDER BY sc.completed_at DESC,sc.id DESC")
|
||||
List<ReturnSkuConversion> listCarton(@Param("customerId") String customerId, @Param("cartonId") long cartonId);
|
||||
}
|
||||
+2
@@ -17,4 +17,6 @@ public class ReturnInventoryRow {
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private Long quantity;
|
||||
private Long relabelledQuantity;
|
||||
private Long packingReservedQuantity;
|
||||
}
|
||||
|
||||
+35
-11
@@ -59,6 +59,7 @@ public class ReturnInventoryService {
|
||||
@Resource private com.baiyue.lastleg.relabel.mapper.RelabelItemMapper relabelItems;
|
||||
@Resource private ReturnInventoryCartonItemMapper cartonItems;
|
||||
@Resource private ReturnInventoryBalanceMapper balances;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper skuConversions;
|
||||
@Resource private ReturnInventoryOperationMapper operations;
|
||||
@Resource private ReturnInventoryQueryMapper queries;
|
||||
@Resource private ReturnInventoryMovementMapper movements;
|
||||
@@ -474,6 +475,7 @@ public class ReturnInventoryService {
|
||||
if (movements.insert(movement) != 1) throw new IllegalStateException("退货出库流水写入失败");
|
||||
if (partial) {
|
||||
content.setQuantity(content.getQuantity() - quantity);
|
||||
content.setRelabelledQuantity(Math.min(value(content.getRelabelledQuantity()), content.getQuantity()));
|
||||
if (cartonItems.updateById(content) != 1) throw conflict("退货箱内数量已变化,请重新核对");
|
||||
}
|
||||
}
|
||||
@@ -581,7 +583,7 @@ public class ReturnInventoryService {
|
||||
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(),
|
||||
relabelProgressStatus(carton), cartons.totalRelabelledQuantity(carton.getId()), cartons.relabelReservedQuantity(carton.getId()))).toList();
|
||||
relabelProgressStatus(carton), completedRelabelQuantity(carton), cartons.relabelReservedQuantity(carton.getId()))).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@@ -603,7 +605,8 @@ public class ReturnInventoryService {
|
||||
(request.page() - 1) * request.size(), request.size()).stream().map(balance -> new ReturnInventoryResponses.PlatformStock(
|
||||
balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(),
|
||||
value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt(),
|
||||
cartonItems.looseAvailableQuantity(balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode()))).toList();
|
||||
cartonItems.looseAvailableQuantity(balance.getCustomerId(), balance.getOverseasWarehouseId(), balance.getSkuCode()),
|
||||
value(balance.getRelabelLockedQuantity()))).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@@ -626,7 +629,7 @@ public class ReturnInventoryService {
|
||||
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(), 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();
|
||||
completedRelabelQuantity(carton), cartons.relabelReservedQuantity(carton.getId()))).toList();
|
||||
return new PageResult<>(records, request.page(), request.size(), total, (total + request.size() - 1) / request.size());
|
||||
}
|
||||
|
||||
@@ -1005,6 +1008,7 @@ public class ReturnInventoryService {
|
||||
}
|
||||
Map<String, Long> remaining = new LinkedHashMap<>(requested);
|
||||
Map<String, Integer> emptiedBySku = new LinkedHashMap<>();
|
||||
Map<String, Long> movedRelabelled = new LinkedHashMap<>();
|
||||
LinkedHashSet<String> consumedBatchNos = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> consumedReturnNos = new LinkedHashSet<>();
|
||||
Map<Long, Map<String, Long>> attachedByOrder = new LinkedHashMap<>();
|
||||
@@ -1014,7 +1018,7 @@ public class ReturnInventoryService {
|
||||
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);
|
||||
boolean hasCompletedRelabel = sourceItems.stream().anyMatch(i -> cartons.completedSkuQuantity(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()));
|
||||
@@ -1028,6 +1032,12 @@ public class ReturnInventoryService {
|
||||
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);
|
||||
if (take > 0) {
|
||||
long labelled = value(item.getRelabelledQuantity());
|
||||
if (labelled > 0 && labelled < value(item.getQuantity()) && take != value(item.getQuantity()))
|
||||
throw conflict("已换标 SKU 只能按当前完整来源明细移箱,不能猜测部分实物来源");
|
||||
long movedLabelled = Math.min(labelled, take);
|
||||
movedRelabelled.merge(sku, movedLabelled, Math::addExact);
|
||||
item.setRelabelledQuantity(labelled - movedLabelled);
|
||||
if (limits != null) limits.get(source.getCartonMark()).put(sku, allowed - take);
|
||||
consumedBatchNos.add(required(source.getSourceBatchNo(), "来源签收批次缺失,无法合箱"));
|
||||
consumedReturnNos.addAll(java.util.Arrays.asList(required(source.getSourceReturnNo(), "来源退货单缺失,无法合箱").split(",")));
|
||||
@@ -1079,8 +1089,11 @@ public class ReturnInventoryService {
|
||||
if (targetItem == null) {
|
||||
targetItem = new ReturnInventoryCartonItem(); targetItem.setId(IdWorker.getId()); targetItem.setCartonId(targetId);
|
||||
targetItem.setCustomerId(customerId); targetItem.setSkuCode(entry.getKey()); targetItem.setProductNameZh(productNames.get(entry.getKey()));
|
||||
targetItem.setQuantity(entry.getValue()); targetItem.setPackingReservedQuantity(0L); cartonItems.insert(targetItem);
|
||||
} else { targetItem.setQuantity(Math.addExact(value(targetItem.getQuantity()), entry.getValue())); cartonItems.updateById(targetItem); }
|
||||
targetItem.setQuantity(entry.getValue()); targetItem.setPackingReservedQuantity(0L);
|
||||
targetItem.setRelabelledQuantity(movedRelabelled.getOrDefault(entry.getKey(), 0L)); cartonItems.insert(targetItem);
|
||||
} else { targetItem.setQuantity(Math.addExact(value(targetItem.getQuantity()), entry.getValue()));
|
||||
targetItem.setRelabelledQuantity(Math.addExact(value(targetItem.getRelabelledQuantity()), movedRelabelled.getOrDefault(entry.getKey(), 0L)));
|
||||
cartonItems.updateById(targetItem); }
|
||||
long cartonDelta = (addedSku ? 1L : 0L) - emptiedBySku.getOrDefault(entry.getKey(), 0);
|
||||
if (balances.adjustCartonQuantity(customerId, target.getOverseasWarehouseId(), entry.getKey(), cartonDelta) != 1)
|
||||
throw new IllegalStateException("退货合箱库存箱数更新失败: " + entry.getKey());
|
||||
@@ -1159,12 +1172,15 @@ public class ReturnInventoryService {
|
||||
private ReturnInventoryResponses.InventoryItem item(ReturnInventoryRow row) {
|
||||
return new ReturnInventoryResponses.InventoryItem(row.getOverseasWarehouseId(), row.getCartonMark(), row.getSourceBatchNo(), row.getStatus(),
|
||||
row.getSkuCode(), row.getProductNameZh(), value(row.getQuantity()), row.getSkuTypes() == null ? 0 : row.getSkuTypes(),
|
||||
value(row.getTotalQuantity()), row.getCreatedAt());
|
||||
value(row.getTotalQuantity()), row.getCreatedAt(), value(row.getRelabelledQuantity()),
|
||||
AVAILABLE.equals(row.getStatus()) ? Math.max(0, value(row.getQuantity()) - value(row.getPackingReservedQuantity())
|
||||
- value(row.getRelabelledQuantity()) - cartons.completedSkuQuantity(row.getCartonId(), row.getSkuCode())) : 0);
|
||||
}
|
||||
|
||||
private ReturnInventoryResponses.SummaryItem summary(com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance balance) {
|
||||
return new ReturnInventoryResponses.SummaryItem(balance.getOverseasWarehouseId(), balance.getSkuCode(), balance.getProductNameZh(),
|
||||
value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt());
|
||||
value(balance.getTotalQuantity()), value(balance.getAvailableQuantity()), value(balance.getCartonQuantity()), balance.getUpdatedAt(),
|
||||
value(balance.getRelabelLockedQuantity()));
|
||||
}
|
||||
|
||||
private static String displayStatus(ReturnInventoryCarton carton) {
|
||||
@@ -1175,14 +1191,22 @@ public class ReturnInventoryService {
|
||||
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity(),
|
||||
"AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
|
||||
? Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity())) : 0)).toList();
|
||||
? Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity())) : 0,
|
||||
value(item.getRelabelledQuantity()), "AVAILABLE".equals(carton.getStatus())
|
||||
? Math.max(0, value(item.getQuantity()) - value(item.getPackingReservedQuantity()) - value(item.getRelabelledQuantity())
|
||||
- cartons.completedSkuQuantity(carton.getId(), item.getSkuCode())) : 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(), 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()));
|
||||
completedRelabelQuantity(carton), cartons.relabelReservedQuantity(carton.getId()),
|
||||
skuConversions.listCarton(carton.getCustomerId(), carton.getId()).stream().map(ReturnInventoryResponses.SkuConversion::from).toList());
|
||||
}
|
||||
|
||||
private String relabelProgressStatus(ReturnInventoryCarton carton) {
|
||||
return ReturnRelabelProgress.status(value(carton.getTotalQuantity()), cartons.totalRelabelledQuantity(carton.getId()), cartons.relabelReservedQuantity(carton.getId()));
|
||||
return ReturnRelabelProgress.status(value(carton.getTotalQuantity()), completedRelabelQuantity(carton), cartons.relabelReservedQuantity(carton.getId()));
|
||||
}
|
||||
private long completedRelabelQuantity(ReturnInventoryCarton carton) {
|
||||
return Math.max(cartons.totalRelabelledQuantity(carton.getId()), cartonItems.listCarton(carton.getId()).stream()
|
||||
.mapToLong(line -> value(line.getRelabelledQuantity()) + cartons.completedSkuQuantity(carton.getId(), line.getSkuCode())).sum());
|
||||
}
|
||||
|
||||
private static String printableHtml(ReturnInventoryCarton carton) {
|
||||
|
||||
+9
-3
@@ -386,7 +386,9 @@ public class ReturnReceiptService {
|
||||
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();
|
||||
.map(i -> new ReturnOrderResponses.Item(i.getSkuCode(), i.getProductNameZh(), remainingRelabelQuantity(carton.getId(), i), remainingRelabelQuantity(carton.getId(), i)))
|
||||
.filter(i -> i.receivedQuantity() > 0).toList();
|
||||
if (items.isEmpty()) continue;
|
||||
result.add(new ReturnOrderResponses.RelabelCandidate(carton.getId(), carton.getCartonMark(), carton.getSourceReturnNo(), carton.getStatus(), value(carton.getTotalQuantity()), items, carton.getRelabelStatus()));
|
||||
}
|
||||
return result;
|
||||
@@ -468,7 +470,7 @@ public class ReturnReceiptService {
|
||||
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();
|
||||
.mapToLong(line -> remainingRelabelQuantity(candidate.getId(), line)).sum();
|
||||
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)
|
||||
@@ -477,7 +479,7 @@ public class ReturnReceiptService {
|
||||
throw conflict("退货 SKU 已被合箱计划锁定,请先完成或取消合箱计划");
|
||||
skuQuantity = cartonItems.lockCarton(carton.getId()).stream()
|
||||
.filter(line -> norm(requested.skuCode()).equals(norm(line.getSkuCode())))
|
||||
.mapToLong(line -> value(line.getQuantity())).sum();
|
||||
.mapToLong(line -> remainingRelabelQuantity(carton.getId(), line)).sum();
|
||||
var sourceSku = List.of(carton.getCartonMark(), norm(requested.skuCode()));
|
||||
skuQuantity -= allocatedBySourceSku.getOrDefault(sourceSku, 0L);
|
||||
if (skuQuantity <= 0) continue;
|
||||
@@ -848,6 +850,10 @@ public class ReturnReceiptService {
|
||||
item.getProductNameZh(), value(item.getExpectedQuantity()), value(item.getReceivedQuantity())); }
|
||||
private PdaReturnReceiptResponses.Item pdaItem(ReturnOrderItem item) { return new PdaReturnReceiptResponses.Item(item.getSkuCode(),
|
||||
item.getProductNameZh(), value(item.getExpectedQuantity()), value(item.getReceivedQuantity())); }
|
||||
private long remainingRelabelQuantity(long cartonId, com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem line) {
|
||||
return Math.max(0, value(line.getQuantity()) - value(line.getPackingReservedQuantity()) - value(line.getRelabelledQuantity())
|
||||
- cartons.completedSkuQuantity(cartonId, line.getSkuCode()));
|
||||
}
|
||||
private static long value(Long value) { return value == null ? 0 : value; }
|
||||
private static String text(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String norm(String value) { return text(value) == null ? null : text(value).toUpperCase(java.util.Locale.ROOT); }
|
||||
|
||||
Reference in New Issue
Block a user