fix(lastleg): complete stock transfer through linked relabel
This commit is contained in:
+70
-6
@@ -36,6 +36,8 @@ import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class InventoryCommandService {
|
||||
private record TransferMoveSnapshot(InventoryCommandRequests.Move move, String sku) {}
|
||||
private record TransferAdjustmentSnapshot(InventoryCommandRequests.Adjustment adjustment, String sku) {}
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private InventoryLedgerMapper ledgers;
|
||||
@@ -237,7 +239,35 @@ public class InventoryCommandService {
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse ship(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return execute(customerId, operatorId, "SHIP", request.idempotencyKey(), request, operation -> {
|
||||
return ship(customerId, operatorId, request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse allocateTransferSnapshot(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request, skuSnapshot);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse pickTransferSnapshot(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return move(customerId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request, skuSnapshot);
|
||||
}
|
||||
|
||||
/** Exact source snapshot comes from the transfer's locked allocation, not a mixed order aggregate. */
|
||||
@Transactional
|
||||
public InventoryCommandResponse shipTransferSnapshot(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return ship(customerId, operatorId, request, skuSnapshot);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse ship(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
return execute(customerId, operatorId, "SHIP", request.idempotencyKey(),
|
||||
skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
@@ -255,7 +285,8 @@ public class InventoryCommandService {
|
||||
balances.updateById(balance);
|
||||
boolean palletEmpty = commands.palletPackages(position.getPalletId()) == 0;
|
||||
if (palletEmpty) warehouseGateway.scrapPallet(customerId, position.getPalletId());
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, balance.getSkuCode(), "SHIP",
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position,
|
||||
skuSnapshot == null ? balance.getSkuCode() : skuSnapshot, "SHIP",
|
||||
"PICKED", "SHIPPED", before, -request.packages(), before - request.packages(),
|
||||
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
|
||||
request.idempotencyKey(), request.remark());
|
||||
@@ -471,10 +502,23 @@ public class InventoryCommandService {
|
||||
@Transactional
|
||||
public InventoryCommandResponse adjust(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Adjustment request) {
|
||||
return adjust(customerId, operatorId, request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse adjustTransferRelabel(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Adjustment request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return adjust(customerId, operatorId, request, skuSnapshot);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse adjust(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Adjustment request, String skuSnapshot) {
|
||||
if (request.packageDelta() == 0) {
|
||||
throw badRequest("Adjustment package delta cannot be zero");
|
||||
}
|
||||
return execute(customerId, operatorId, "ADJUST", request.idempotencyKey(), request, operation -> {
|
||||
return execute(customerId, operatorId, "ADJUST", request.idempotencyKey(),
|
||||
skuSnapshot == null ? request : new TransferAdjustmentSnapshot(request, skuSnapshot), operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
@@ -492,7 +536,8 @@ public class InventoryCommandService {
|
||||
balance.setAvailablePackages(balanceAfter);
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, balance.getSkuCode(), "ADJUST",
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position,
|
||||
skuSnapshot == null ? balance.getSkuCode() : skuSnapshot, "ADJUST",
|
||||
"AVAILABLE", "AVAILABLE", before, request.packageDelta(), after,
|
||||
request.businessOrderNo(), null, null, null, request.idempotencyKey(),
|
||||
request.reason() + remarkSuffix(request.remark()));
|
||||
@@ -507,7 +552,25 @@ public class InventoryCommandService {
|
||||
|
||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||
Bucket from, Bucket to, InventoryCommandRequests.Move request) {
|
||||
return execute(customerId, operatorId, operationType, request.idempotencyKey(), request, operation -> {
|
||||
return move(customerId, operatorId, operationType, from, to, request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse releaseTransferRelabel(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
requireTransferSku(request.idempotencyKey(), skuSnapshot);
|
||||
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request, skuSnapshot);
|
||||
}
|
||||
|
||||
private static void requireTransferSku(String key, String skuSnapshot) {
|
||||
if (key == null || !key.startsWith("WAREHOUSE_TRANSFER:") || skuSnapshot == null || skuSnapshot.isBlank()
|
||||
|| "MIXED".equals(skuSnapshot)) throw badRequest("Exact warehouse transfer SKU snapshot is required");
|
||||
}
|
||||
|
||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||
Bucket from, Bucket to, InventoryCommandRequests.Move request, String skuSnapshot) {
|
||||
return execute(customerId, operatorId, operationType, request.idempotencyKey(),
|
||||
skuSnapshot == null ? request : new TransferMoveSnapshot(request, skuSnapshot), operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
@@ -524,7 +587,8 @@ public class InventoryCommandService {
|
||||
packages(balance, to, packages(balance, to) + request.packages());
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, balance.getSkuCode(), operationType,
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position,
|
||||
skuSnapshot == null ? balance.getSkuCode() : skuSnapshot, operationType,
|
||||
from.name(), to.name(), beforeTarget, request.packages(), beforeTarget + request.packages(),
|
||||
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
|
||||
request.idempotencyKey(), request.remark());
|
||||
|
||||
+12
-4
@@ -102,6 +102,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private OutboundPdaTaskMapper outboundTasks;
|
||||
@Resource private OverseasFeeSourceService feeSources;
|
||||
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferRelabelCompletionService transferRelabelCompletion;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String customerId(String relabelNo) { return require(null, relabelNo).getCustomerId(); }
|
||||
@@ -812,6 +813,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
throw conflict("Upload the shipping label before confirming relabel completion");
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
WarehouseTransferOrder linkedTransfer = warehouseTransfers == null ? null
|
||||
: warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
boolean existingStockTransfer = linkedTransfer != null
|
||||
&& StringUtils.hasText(linkedTransfer.getAllocationIdempotencyKey());
|
||||
boolean requiresOutbound = relabelItems.stream()
|
||||
.anyMatch(item -> Set.of("CARTON_LABEL", "CARTON_AND_SKU").contains(operationType(item)));
|
||||
reviewTask.setStatus("COMPLETED"); reviewTask.setReviewDecision("CONFIRMED");
|
||||
@@ -827,14 +832,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setFulfillmentStatus(requiresOutbound
|
||||
? ("TRUCK".equals(application.getDeliveryMethod()) ? "WAIT_PRE_DISPATCH" : "WAIT_OUTBOUND_CREATION")
|
||||
: "NOT_REQUIRED");
|
||||
if (requiresOutbound && !"TRUCK".equals(application.getDeliveryMethod())) {
|
||||
if (requiresOutbound && !existingStockTransfer && !"TRUCK".equals(application.getDeliveryMethod())) {
|
||||
createDirectOutbound(application, operatorId, now);
|
||||
application.setFulfillmentStatus("OUTBOUND_CREATED");
|
||||
}
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
WarehouseTransferOrder linkedTransfer = warehouseTransfers == null ? null
|
||||
: warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
for (LastlegOrder order : tailOrders(application)) {
|
||||
if ("RETURN_RELABEL".equals(order.getOrderType()) && !Objects.equals(order.getRelabelOrderNo(), application.getRelabelNo())) continue;
|
||||
// 仓储转运尾程单由下方按转运目的仓一次性写入精确状态,避免先落入通用状态。
|
||||
@@ -910,13 +913,18 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
|
||||
String transferStatus = transferHasDestination
|
||||
? "READY_FOR_DISPATCH" : "WAIT_FBA_DESTINATION";
|
||||
if (existingStockTransfer) {
|
||||
transferRelabelCompletion.complete(transfer, application, relabelItems, operatorId, now);
|
||||
transferStatus = "ALLOCATED";
|
||||
}
|
||||
transfer.setStatus(transferStatus);
|
||||
transfer.setRelabelCompletedAt(now);
|
||||
transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
boolean relabelHasDestination = !relabelItems.isEmpty() && relabelItems.stream()
|
||||
.allMatch(item -> StringUtils.hasText(item.getDestinationFbaWarehouseCode()));
|
||||
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus) || relabelHasDestination
|
||||
application.setFulfillmentStatus(existingStockTransfer ? "WAIT_TRANSFER_OUTBOUND"
|
||||
: "READY_FOR_DISPATCH".equals(transferStatus) || relabelHasDestination
|
||||
? "WAIT_PRE_DISPATCH" : transferStatus);
|
||||
applications.updateById(application);
|
||||
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ public final class WarehouseTransferResponses {
|
||||
public record SourceOrder(Long lastlegOrderId, String lastlegOrderNo, Long inboundOrderId, String inboundCode) {}
|
||||
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
|
||||
Long palletId, String palletNo, int packageCount, String status,
|
||||
Long destinationInventoryPositionId) {}
|
||||
Long destinationInventoryPositionId, String skuCode) {}
|
||||
public record Summary(Long id, String transferNo, String inboundCode, Long lastlegOrderId,
|
||||
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
|
||||
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
|
||||
|
||||
+4
@@ -3,6 +3,8 @@ package com.baiyue.lastleg.transfer.entity;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -20,7 +22,9 @@ public class WarehouseTransferInventoryAllocation {
|
||||
private Long palletId;
|
||||
private String palletNo;
|
||||
private Integer packageCount;
|
||||
private String skuCode;
|
||||
private String status;
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String activePositionKey;
|
||||
private LocalDateTime allocatedAt;
|
||||
private LocalDateTime shippedAt;
|
||||
|
||||
+2
@@ -21,5 +21,7 @@ public class WarehouseTransferRelabelBinding {
|
||||
private String fbaCartonMark;
|
||||
private String destinationWarehouseCode;
|
||||
private String remark;
|
||||
private Long sourceAllocationId;
|
||||
private Long relabelItemId;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface WarehouseTransferInventoryAllocationMapper extends BaseMapper<WarehouseTransferInventoryAllocation> {
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_inventory_allocation WHERE customer_id=#{customerId} " +
|
||||
"AND transfer_order_id=#{transferOrderId} ORDER BY id FOR UPDATE")
|
||||
"AND transfer_order_id=#{transferOrderId} AND status<>'RELABELLED' ORDER BY id FOR UPDATE")
|
||||
List<WarehouseTransferInventoryAllocation> lockTransfer(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferRelabelBindingMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** Runs in customer confirmation's transaction; preserves original selections and follows actual pallet moves. */
|
||||
@Service
|
||||
public class WarehouseTransferRelabelCompletionService {
|
||||
@Resource private WarehouseTransferInventoryAllocationMapper allocations;
|
||||
@Resource private WarehouseTransferRelabelBindingMapper bindings;
|
||||
@Resource private InventoryCommandMapper inventoryRows;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
private record Target(InventoryBalance balance, InventoryPosition position, int packages, String sku) {}
|
||||
|
||||
public void complete(WarehouseTransferOrder transfer, RelabelApplication application,
|
||||
List<RelabelItem> items, String operator, LocalDateTime now) {
|
||||
String customer = transfer.getCustomerId();
|
||||
if (!Objects.equals(application.getCustomerId(), customer)
|
||||
|| !Objects.equals(application.getId(), transfer.getRelabelApplicationId())
|
||||
|| !Objects.equals(application.getLastlegOrderId(), transfer.getLastlegOrderId())
|
||||
|| application.getCustomerConfirmedAt() == null) throw conflict("本票换标确认归属不一致");
|
||||
List<WarehouseTransferInventoryAllocation> original = allocations.lockTransfer(customer, transfer.getId());
|
||||
Map<Long, WarehouseTransferInventoryAllocation> byId = new HashMap<>();
|
||||
Map<Long, Integer> remaining = new HashMap<>();
|
||||
for (var row : original) {
|
||||
if (!"RESERVED".equals(row.getStatus())) throw conflict("转运预留已被其他操作处理");
|
||||
byId.put(row.getId(), row); remaining.put(row.getId(), row.getPackageCount());
|
||||
}
|
||||
Map<Long, RelabelItem> itemById = new HashMap<>(); items.forEach(item -> itemById.put(item.getId(), item));
|
||||
var sources = bindings.listOrder(customer, transfer.getId());
|
||||
if (sources.isEmpty()) throw conflict("本票换标来源绑定缺失");
|
||||
Map<Long, Target> targets = new LinkedHashMap<>();
|
||||
Set<Long> seen = new HashSet<>();
|
||||
for (var source : sources) {
|
||||
var allocation = byId.get(source.getSourceAllocationId());
|
||||
RelabelItem item = itemById.get(source.getRelabelItemId());
|
||||
if (allocation == null || item == null || !seen.add(item.getId())
|
||||
|| !Objects.equals(item.getCustomerId(), customer)
|
||||
|| !Objects.equals(item.getApplicationId(), application.getId())
|
||||
|| !Objects.equals(item.getSourceInboundOrderId(), allocation.getInboundOrderId())
|
||||
|| !Objects.equals(item.getCartonMark(), source.getCustomerCartonMark())
|
||||
|| !Objects.equals(item.getSkuCode(), source.getOriginalSku())
|
||||
|| !Objects.equals(item.getSkuCode(), allocation.getSkuCode()))
|
||||
throw conflict("本票换标箱未绑定到原转运预留");
|
||||
int count = Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0);
|
||||
int left = remaining.get(allocation.getId()) - count;
|
||||
if (count <= 0 || left < 0) throw conflict("本票换标数量超过预留来源");
|
||||
remaining.put(allocation.getId(), left);
|
||||
InventoryPosition current = inventoryRows.lockPosition(customer, item.getSourceInventoryPositionId());
|
||||
InventoryBalance balance = current == null ? null : inventoryRows.lockBalance(customer, current.getBalanceId());
|
||||
if (balance == null || !Objects.equals(balance.getInboundOrderId(), allocation.getInboundOrderId())
|
||||
|| !Objects.equals(balance.getOverseasWarehouseId(), transfer.getOperatingWarehouseId())
|
||||
|| current.getHoldPackages() == null || current.getHoldPackages() < count)
|
||||
throw conflict("本票换标实物位置或冻结数量不一致");
|
||||
if (allocations.countActiveDispatchLock(customer, current.getPalletId()) > 0)
|
||||
throw conflict("本票换标托盘已被排车占用");
|
||||
String key = "WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL:" + item.getId();
|
||||
inventory.releaseTransferRelabel(customer, operator, new InventoryCommandRequests.Move(current.getId(), count,
|
||||
transfer.getTransferNo(), null, key, key + ":RELEASE", "本票换标客户确认"), item.getSkuCode());
|
||||
String outputSku = source.getNewSku() == null ? item.getSkuCode() : source.getNewSku();
|
||||
if (!outputSku.equals(item.getSkuCode())) {
|
||||
inventory.adjustTransferRelabel(customer, operator, new InventoryCommandRequests.Adjustment(current.getId(), -count,
|
||||
"WAREHOUSE_TRANSFER_RELABEL", transfer.getTransferNo(), key + ":SOURCE", "本票换标旧SKU转出"), item.getSkuCode());
|
||||
var output = inventory.stockIn(customer, operator, new InventoryCommandRequests.StockIn(customer,
|
||||
balance.getCustomerName(), balance.getOverseasWarehouseId(), balance.getOverseasWarehouseCode(),
|
||||
balance.getOverseasWarehouseName(), transfer.getDestinationWarehouseId(),
|
||||
source.getDestinationWarehouseCode() == null ? balance.getDestinationWarehouseCode() : source.getDestinationWarehouseCode(),
|
||||
source.getDestinationWarehouseCode() == null ? balance.getDestinationWarehouseName() : source.getDestinationWarehouseCode(),
|
||||
balance.getInboundOrderId(), balance.getInboundOrderNo(), balance.getInboundCode(), outputSku,
|
||||
balance.getProductNameZh(), balance.getProductNameEn(), current.getPalletId(), current.getPalletNo(),
|
||||
key + ":OUTPUT", current.getWarehouseLocationId(), current.getWarehouseLocationCode(), count,
|
||||
transfer.getTransferNo(), key, key + ":TARGET", "本票换标新SKU转入"));
|
||||
current = inventoryRows.lockPosition(customer, output.positionId());
|
||||
balance = inventoryRows.lockBalance(customer, output.balanceId());
|
||||
if (current == null || balance == null || !Objects.equals(current.getBalanceId(), balance.getId())
|
||||
|| !Objects.equals(balance.getInboundOrderId(), allocation.getInboundOrderId()))
|
||||
throw conflict("换标新SKU库存事实不一致");
|
||||
}
|
||||
addTarget(targets, balance, current, count, outputSku);
|
||||
}
|
||||
if (seen.size() != items.size()) throw conflict("换标明细存在未绑定本票的来源");
|
||||
for (var row : original) {
|
||||
int count = remaining.get(row.getId());
|
||||
if (count > 0) {
|
||||
var current = inventoryRows.lockPosition(customer, row.getInventoryPositionId());
|
||||
var balance = inventoryRows.lockBalance(customer, row.getInventoryBalanceId());
|
||||
if (current == null || balance == null || current.getHoldPackages() == null || current.getHoldPackages() < count)
|
||||
throw conflict("未换标的转运余量不可用");
|
||||
String key = "WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":UNLABELLED:" + row.getId();
|
||||
inventory.releaseTransferRelabel(customer, operator, new InventoryCommandRequests.Move(current.getId(), count,
|
||||
transfer.getTransferNo(), null, key, key + ":RELEASE", "本票未换标余量确认"), row.getSkuCode());
|
||||
addTarget(targets, balance, current, count, row.getSkuCode());
|
||||
}
|
||||
row.setStatus("RELABELLED"); row.setActivePositionKey(null); row.setUpdatedAt(now); allocations.updateById(row);
|
||||
}
|
||||
if (targets.values().stream().mapToInt(Target::packages).sum() != transfer.getPackageCount())
|
||||
throw conflict("换标后转运总箱数不守恒");
|
||||
for (Target target : targets.values()) {
|
||||
var row = new WarehouseTransferInventoryAllocation(); row.setId(IdWorker.getId());
|
||||
row.setTransferOrderId(transfer.getId()); row.setCustomerId(customer);
|
||||
row.setInventoryPositionId(target.position().getId()); row.setInventoryBalanceId(target.balance().getId());
|
||||
row.setInboundOrderId(target.balance().getInboundOrderId()); row.setInboundCode(target.balance().getInboundCode());
|
||||
row.setPalletId(target.position().getPalletId()); row.setPalletNo(target.position().getPalletNo());
|
||||
row.setPackageCount(target.packages()); row.setStatus("RESERVED");
|
||||
row.setSkuCode(target.sku());
|
||||
row.setActivePositionKey(customer + ":" + target.position().getId()); row.setAllocatedAt(now);
|
||||
row.setCreatedAt(now); row.setUpdatedAt(now); allocations.insert(row);
|
||||
}
|
||||
}
|
||||
private static void addTarget(Map<Long, Target> targets, InventoryBalance balance, InventoryPosition position, int count, String sku) {
|
||||
Target previous = targets.get(position.getId());
|
||||
if (previous != null && !Objects.equals(previous.sku(), sku)) throw conflict("不同SKU换标产出必须保留独立位置批次");
|
||||
targets.put(position.getId(), new Target(balance, position, count + (previous == null ? 0 : previous.packages()), sku));
|
||||
}
|
||||
private static ResponseStatusException conflict(String text) { return new ResponseStatusException(HttpStatus.CONFLICT, text); }
|
||||
}
|
||||
+94
-13
@@ -562,12 +562,13 @@ public class WarehouseTransferService {
|
||||
new LambdaQueryWrapper<WarehouseTransferInventoryAllocation>()
|
||||
.eq(WarehouseTransferInventoryAllocation::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferInventoryAllocation::getTransferOrderId, transfer.getId())
|
||||
.ne(WarehouseTransferInventoryAllocation::getStatus, "RELABELLED")
|
||||
.orderByAsc(WarehouseTransferInventoryAllocation::getId))
|
||||
.stream().map(value -> {
|
||||
WarehouseTransferReceipt receipt = receiptByAllocation.get(value.getId());
|
||||
return new WarehouseTransferResponses.Allocation(value.getInventoryPositionId(), value.getInboundOrderId(),
|
||||
value.getInboundCode(), value.getPalletId(), value.getPalletNo(), value.getPackageCount(),
|
||||
value.getStatus(), receipt == null ? null : receipt.getDestinationInventoryPositionId());
|
||||
value.getStatus(), receipt == null ? null : receipt.getDestinationInventoryPositionId(), value.getSkuCode());
|
||||
}).toList();
|
||||
List<WarehouseTransferResponses.MeasurementPhoto> photos = measurementPhotos.listSubmitted(customerId, transfer.getId())
|
||||
.stream().map(value -> new WarehouseTransferResponses.MeasurementPhoto(value.getFileId(), value.getFileName(),
|
||||
@@ -1036,7 +1037,7 @@ public class WarehouseTransferService {
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
if (ALLOCATED.equals(transfer.getStatus()) && key.equals(transfer.getAllocationIdempotencyKey())) {
|
||||
if (key.equals(transfer.getAllocationIdempotencyKey())) {
|
||||
if (!requestHash.equals(transfer.getAllocationRequestHash())) {
|
||||
throw conflict("Allocation idempotency key was reused with different inventory positions");
|
||||
}
|
||||
@@ -1065,6 +1066,7 @@ public class WarehouseTransferService {
|
||||
if (sourceInboundIds.isEmpty()) throw conflict("Warehouse transfer has no source inbound orders");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Map<String, List<ConfirmedStockSource>> selectedRelabelSources = new java.util.LinkedHashMap<>();
|
||||
for (WarehouseTransferRequests.Allocation source : requested) {
|
||||
InventoryPosition position = inventoryCommands.lockPosition(customerId, source.inventoryPositionId());
|
||||
if (position == null) throw notFound("Inventory position not found: " + source.inventoryPositionId());
|
||||
@@ -1088,6 +1090,7 @@ public class WarehouseTransferService {
|
||||
allocation.setInventoryBalanceId(balance.getId()); allocation.setInboundOrderId(balance.getInboundOrderId());
|
||||
allocation.setInboundCode(balance.getInboundCode()); allocation.setPalletId(position.getPalletId());
|
||||
allocation.setPalletNo(position.getPalletNo()); allocation.setPackageCount(source.packageCount());
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) allocation.setSkuCode(balance.getSkuCode());
|
||||
allocation.setStatus("RESERVED"); allocation.setActivePositionKey(customerId + ":" + position.getId());
|
||||
allocation.setAllocatedAt(now); allocation.setCreatedAt(now); allocation.setUpdatedAt(now);
|
||||
try {
|
||||
@@ -1095,12 +1098,42 @@ public class WarehouseTransferService {
|
||||
} catch (DuplicateKeyException exception) {
|
||||
throw conflict("Inventory position is already reserved by another warehouse transfer");
|
||||
}
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()) && "MIXED".equals(balance.getSkuCode()))
|
||||
throw conflict("聚合库存不能证明本票旧SKU,请选择有明确SKU来源的库存");
|
||||
selectedRelabelSources.computeIfAbsent(balance.getSkuCode(), ignored -> new ArrayList<>())
|
||||
.add(new ConfirmedStockSource(balance.getId(), position.getId(), source.packageCount(),
|
||||
balance.getInboundOrderId(), allocation.getId()));
|
||||
}
|
||||
transfer.setStatus(ALLOCATED); transfer.setAllocationIdempotencyKey(key);
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
List<WarehouseTransferItem> cargo = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()));
|
||||
if (relabelBindings.listOrder(customerId, transfer.getId()).isEmpty())
|
||||
throw conflict("Required transfer relabel instructions are missing");
|
||||
RelabelResponses.Detail linked = createLinkedRelabel(customerId, operatorId, transfer,
|
||||
cargo.stream().collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity())),
|
||||
selectedRelabelSources);
|
||||
transfer.setRelabelApplicationId(linked.application().id());
|
||||
transfer.setRelabelNo(linked.application().relabelNo());
|
||||
transfer.setStatus(WAIT_RELABEL);
|
||||
for (var entry : selectedRelabelSources.entrySet()) {
|
||||
for (ConfirmedStockSource source : entry.getValue()) {
|
||||
if (source.remainingPackages <= 0) continue;
|
||||
String holdKey = "WAREHOUSE_TRANSFER:" + transferNo + ":UNLABELLED:" + source.allocationId;
|
||||
inventory.hold(customerId, operatorId, new InventoryCommandRequests.Move(source.positionId,
|
||||
source.remainingPackages, transferNo, null, holdKey, holdKey + ":HOLD", "本票转运未换标余量预留"));
|
||||
}
|
||||
}
|
||||
} else transfer.setStatus(ALLOCATED);
|
||||
transfer.setAllocationIdempotencyKey(key);
|
||||
transfer.setAllocationRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
String before = order.getCurrentStatus(); order.setCurrentStatus("WAIT_OUTBOUND");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:ALLOCATED"); order.setUpdatedAt(now);
|
||||
String before = order.getCurrentStatus(); order.setCurrentStatus(
|
||||
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? INBOUND_COMPLETED : "WAIT_OUTBOUND");
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
order.setRelabelOrderNo(transfer.getRelabelNo()); order.setRelabelStatus("PENDING_REVIEW");
|
||||
}
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus()); order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
"Warehouse transfer inventory allocated", now);
|
||||
@@ -1124,6 +1157,14 @@ public class WarehouseTransferService {
|
||||
if (!ALLOCATED.equals(transfer.getStatus())) {
|
||||
throw conflict("Warehouse transfer inventory must be allocated before outbound");
|
||||
}
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
RelabelApplication linked = StringUtils.hasText(transfer.getRelabelNo())
|
||||
? relabelApplications.lockNo(customerId, transfer.getRelabelNo()) : null;
|
||||
if (linked == null || !Objects.equals(linked.getId(), transfer.getRelabelApplicationId())
|
||||
|| !Objects.equals(linked.getLastlegOrderId(), transfer.getLastlegOrderId())
|
||||
|| linked.getCustomerConfirmedAt() == null || transfer.getRelabelCompletedAt() == null)
|
||||
throw conflict("本转运单换标尚未完成客户确认,不可出库");
|
||||
}
|
||||
if (!StringUtils.hasText(transfer.getFbaWarehouseCode())) {
|
||||
throw conflict("A destination warehouse is required before outbound");
|
||||
}
|
||||
@@ -1139,13 +1180,21 @@ public class WarehouseTransferService {
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(
|
||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":ALLOCATE", trim(request.remark()));
|
||||
inventory.allocate(customerId, operatorId, move);
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()))
|
||||
inventory.allocateTransferSnapshot(customerId, operatorId, move, allocation.getSkuCode());
|
||||
else inventory.allocate(customerId, operatorId, move);
|
||||
var pick = new InventoryCommandRequests.Move(
|
||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark())));
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark()));
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()))
|
||||
inventory.pickTransferSnapshot(customerId, operatorId, pick, allocation.getSkuCode());
|
||||
else inventory.pick(customerId, operatorId, pick);
|
||||
var ship = new InventoryCommandRequests.Move(
|
||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark())));
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark()));
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel()))
|
||||
inventory.shipTransferSnapshot(customerId, operatorId, ship, allocation.getSkuCode());
|
||||
else inventory.ship(customerId, operatorId, ship);
|
||||
allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null);
|
||||
allocation.setShippedAt(now); allocation.setUpdatedAt(now); inventoryAllocations.updateById(allocation);
|
||||
}
|
||||
@@ -1158,6 +1207,10 @@ public class WarehouseTransferService {
|
||||
orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
"Warehouse transfer left source warehouse", now);
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
RelabelApplication linked = relabelApplications.lockNo(customerId, transfer.getRelabelNo());
|
||||
linked.setFulfillmentStatus("OUTBOUNDED"); linked.setUpdatedAt(now); relabelApplications.updateById(linked);
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@@ -1200,7 +1253,8 @@ public class WarehouseTransferService {
|
||||
trim(request.destinationOverseasWarehouseName()), trim(request.destinationWarehouseId()),
|
||||
request.destinationWarehouseCode().trim(), trim(request.destinationWarehouseName()),
|
||||
transfer.getLastlegOrderId(), transfer.getLastlegOrderNo(), transferNo,
|
||||
sourceBalance.getSkuCode(), sourceBalance.getProductNameZh(), sourceBalance.getProductNameEn(),
|
||||
Boolean.TRUE.equals(transfer.getNeedRelabel()) ? allocation.getSkuCode() : sourceBalance.getSkuCode(),
|
||||
sourceBalance.getProductNameZh(), sourceBalance.getProductNameEn(),
|
||||
request.destinationPalletId(), request.destinationPalletNo().trim(),
|
||||
trim(request.batchNo()) == null ? transferNo : request.batchNo().trim(), request.warehouseLocationId(),
|
||||
trim(request.warehouseLocationCode()), allocation.getPackageCount(), transferNo,
|
||||
@@ -1225,6 +1279,13 @@ public class WarehouseTransferService {
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
|
||||
"Warehouse transfer inventory received at destination", now);
|
||||
if (complete && Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
RelabelApplication linked = relabelApplications.lockNo(customerId, transfer.getRelabelNo());
|
||||
if (linked == null || !Objects.equals(linked.getId(), transfer.getRelabelApplicationId()))
|
||||
throw conflict("本票换标完成归属不一致");
|
||||
linked.setStatus("COMPLETED"); linked.setFulfillmentStatus("COMPLETED");
|
||||
linked.setUpdatedAt(now); relabelApplications.updateById(linked);
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@@ -1616,7 +1677,13 @@ public class WarehouseTransferService {
|
||||
List<RelabelRequests.SkuItem> skuItems = Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(binding.getOperationType())
|
||||
? List.of(new RelabelRequests.SkuItem(effectiveSku, binding.getNewSku(),
|
||||
binding.getQuantity(), binding.getRemark())) : List.of();
|
||||
items.add(new RelabelRequests.Item(stockSource.balanceId, stockSource.positionId, transfer.getInboundOrderId(),
|
||||
if (stockSource.allocationId != null) {
|
||||
binding.setOriginalSku(effectiveSku);
|
||||
binding.setSourceAllocationId(stockSource.allocationId);
|
||||
relabelBindings.updateById(binding);
|
||||
}
|
||||
items.add(new RelabelRequests.Item(stockSource.balanceId, stockSource.positionId,
|
||||
stockSource.inboundId == null ? transfer.getInboundOrderId() : stockSource.inboundId,
|
||||
effectiveSku, null, null, null, source.getProductNameZh(), 1, 1,
|
||||
StringUtils.hasText(transfer.getFbaShipmentNo()) ? transfer.getFbaShipmentNo() : transfer.getTransferNo(),
|
||||
transfer.getTrackingNo(), binding.getNewSku(), "仓储转运自动换标", binding.getRemark(),
|
||||
@@ -1628,8 +1695,16 @@ public class WarehouseTransferService {
|
||||
transfer.getRelabelContactPhone(), transfer.getRelabelChargeAmount(), "仓储转运确认后自动创建换标单",
|
||||
"WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL", items,
|
||||
firstText(transfer.getRelabelDeliveryMethod(), "TRUCK"), false);
|
||||
return relabelService.createForExistingTailOrder(customerId, operatorId,
|
||||
RelabelResponses.Detail created = relabelService.createForExistingTailOrder(customerId, operatorId,
|
||||
transfer.getLastlegOrderId(), request);
|
||||
for (WarehouseTransferRelabelBinding binding : relabelBindings.listOrder(customerId, transfer.getId())) {
|
||||
if (binding.getSourceAllocationId() == null) continue;
|
||||
RelabelResponses.Item item = created.items().stream()
|
||||
.filter(value -> Objects.equals(value.cartonMark(), binding.getCustomerCartonMark()))
|
||||
.findFirst().orElseThrow(() -> conflict("Created relabel did not preserve the transfer carton"));
|
||||
binding.setRelabelItemId(item.id()); relabelBindings.updateById(binding);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
private static ConfirmedStockSource nextStockSource(List<ConfirmedStockSource> sources) {
|
||||
@@ -1647,12 +1722,18 @@ public class WarehouseTransferService {
|
||||
private final Long balanceId;
|
||||
private final Long positionId;
|
||||
private int remainingPackages;
|
||||
private Long inboundId;
|
||||
private Long allocationId;
|
||||
|
||||
private ConfirmedStockSource(Long balanceId, Long positionId, int remainingPackages) {
|
||||
this.balanceId = balanceId;
|
||||
this.positionId = positionId;
|
||||
this.remainingPackages = remainingPackages;
|
||||
}
|
||||
private ConfirmedStockSource(Long balanceId, Long positionId, int remainingPackages,
|
||||
Long inboundId, Long allocationId) {
|
||||
this(balanceId, positionId, remainingPackages); this.inboundId = inboundId; this.allocationId = allocationId;
|
||||
}
|
||||
}
|
||||
|
||||
private InboundPlan ensureTransferInboundPlan(String customerId, WarehouseTransferOrder transfer,
|
||||
|
||||
Reference in New Issue
Block a user