提交
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.lastleg.transfer.repository;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 封装仓储转运所需的退货来源查询,保留客户和操作仓隔离。 */
|
||||
@Repository
|
||||
public class TransferReturnSourceRepository {
|
||||
@Resource private ReturnOrderMapper orders;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper items;
|
||||
|
||||
/** 按当前客户和退货单号查询来源单据。 */
|
||||
public ReturnOrder findOrder(String customerId, String returnNo) {
|
||||
return orders.findNo(customerId, returnNo);
|
||||
}
|
||||
|
||||
/** 查询当前仓内已完成清点且可用的退货箱,不影响其他页面的历史库存查询。 */
|
||||
public List<ReturnInventoryCarton> availableCartons(String customerId, String warehouseId, String returnNo) {
|
||||
return cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
|
||||
&& carton.getRelabelApplicationId() == null)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 批量读取已通过客户及仓库隔离校验的来源箱明细。 */
|
||||
public List<ReturnInventoryCartonItem> cartonItems(List<Long> cartonIds) {
|
||||
return cartonIds.isEmpty() ? List.of() : items.listCartons(cartonIds);
|
||||
}
|
||||
}
|
||||
+66
-14
@@ -59,6 +59,10 @@ import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletItemMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferRelabelBindingMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferMeasurementPhotoMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.transfer.repository.TransferReturnSourceRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -88,6 +92,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class WarehouseTransferService {
|
||||
/** 退货单在转运来源中的展示编码,避免把退货单号误当成普通入仓码查询。 */
|
||||
public static final String RETURN_ORDER_PREFIX = "RETURN_ORDER:";
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge fleetPool;
|
||||
@@ -140,10 +146,32 @@ public class WarehouseTransferService {
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private InventoryCommandMapper inventoryCommands;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private TransferReturnSourceRepository returnSources;
|
||||
|
||||
/** 查询来源单据当前可用的箱唛及箱内 SKU,兼容普通入仓与退货库存。 */
|
||||
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
if (!StringUtils.hasText(warehouseId) || !StringUtils.hasText(inboundCode)) throw badRequest("请选择海外仓和库存来源单据");
|
||||
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), inboundCode.trim()).stream().map(row -> {
|
||||
String source = inboundCode.trim();
|
||||
if (isReturnOrderSource(source)) {
|
||||
String returnNo = returnNoFromSource(source);
|
||||
ReturnOrder order = returnSources.findOrder(customerId, returnNo);
|
||||
if (order == null || !warehouseId.trim().equals(order.getOverseasWarehouseId())) {
|
||||
throw notFound("退货单不存在或不属于当前操作仓");
|
||||
}
|
||||
List<ReturnInventoryCarton> cartons = returnSources.availableCartons(customerId, warehouseId.trim(), returnNo);
|
||||
List<Long> cartonIds = cartons.stream().map(ReturnInventoryCarton::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, List<ReturnInventoryCartonItem>> items = cartonIds.isEmpty() ? Map.of()
|
||||
: returnSources.cartonItems(cartonIds).stream().collect(Collectors.groupingBy(ReturnInventoryCartonItem::getCartonId));
|
||||
return cartons.stream().map(carton -> {
|
||||
List<ReturnInventoryCartonItem> rows = items.getOrDefault(carton.getId(), List.of());
|
||||
List<WarehouseTransferResponses.SourceCartonSku> contents = rows.stream()
|
||||
.filter(item -> item.getQuantity() != null && item.getQuantity() > 0)
|
||||
.map(item -> new WarehouseTransferResponses.SourceCartonSku(item.getSkuCode(), item.getQuantity())).toList();
|
||||
return new WarehouseTransferResponses.SourceCarton(carton.getCartonMark(), null, carton.getTotalQuantity(),
|
||||
carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), contents);
|
||||
}).toList();
|
||||
}
|
||||
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source).stream().map(row -> {
|
||||
var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson());
|
||||
var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList();
|
||||
if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity()));
|
||||
@@ -161,6 +189,7 @@ public class WarehouseTransferService {
|
||||
return StringUtils.hasText(request.inboundCode()) ? "" : request.deliveryAddress().trim();
|
||||
}
|
||||
|
||||
/** 创建转运草稿并关联来源单据,退货来源沿其关联尾程订单核对客户归属。 */
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail create(String customerId, String operatorId,
|
||||
WarehouseTransferRequests.Create request) {
|
||||
@@ -203,18 +232,35 @@ public class WarehouseTransferService {
|
||||
List<LastlegOrder> selectedOrders;
|
||||
if (existingInventoryTransfer) {
|
||||
inboundCode = requestedInboundCode;
|
||||
inboundOrderId = inboundSnapshots.findId(customerId, inboundCode);
|
||||
if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode);
|
||||
if (isReturnOrderSource(inboundCode)) {
|
||||
ReturnOrder returnOrder = returnSources.findOrder(customerId, returnNoFromSource(inboundCode));
|
||||
if (returnOrder == null || !request.operatingWarehouseId().trim().equals(returnOrder.getOverseasWarehouseId())
|
||||
|| returnOrder.getLastlegOrderId() == null) {
|
||||
throw notFound("退货单不存在、操作仓不匹配或未关联尾程订单");
|
||||
}
|
||||
LastlegOrder returnOrderTail = orders.selectById(returnOrder.getLastlegOrderId());
|
||||
if (returnOrderTail == null || !customerId.equals(returnOrderTail.getCustomerId())) {
|
||||
throw notFound("退货单关联的尾程订单不存在");
|
||||
}
|
||||
inboundOrderId = StringUtils.hasText(returnOrderTail.getSourceInboundId())
|
||||
? inboundSnapshots.findBySourceInboundId(customerId, returnOrderTail.getSourceInboundId())
|
||||
: inboundSnapshots.findId(customerId, returnOrderTail.getInboundCode());
|
||||
if (inboundOrderId == null) throw notFound("退货单关联的入仓快照不存在");
|
||||
selectedOrders = List.of(returnOrderTail);
|
||||
} else {
|
||||
inboundOrderId = inboundSnapshots.findId(customerId, inboundCode);
|
||||
if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode);
|
||||
selectedOrders = selectedIds.isEmpty()
|
||||
? orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getInboundCode, inboundCode))
|
||||
: orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds));
|
||||
}
|
||||
// The merchant form may only submit the inventory inbound code. In that
|
||||
// case derive all eligible tail orders for the inbound instead of
|
||||
// rejecting a valid inventory transfer because source IDs were omitted.
|
||||
selectedOrders = selectedIds.isEmpty()
|
||||
? orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getInboundCode, inboundCode))
|
||||
: orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds));
|
||||
if (selectedOrders.isEmpty() || (!selectedIds.isEmpty() && selectedOrders.size() != selectedIds.size())) {
|
||||
if (selectedOrders.isEmpty() || (!selectedIds.isEmpty() && !isReturnOrderSource(inboundCode) && selectedOrders.size() != selectedIds.size())) {
|
||||
throw notFound("One or more selected customer orders do not exist in the current tenant");
|
||||
}
|
||||
} else {
|
||||
@@ -393,6 +439,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
/** 提交草稿前重新校验来源箱归属,防止草稿保存后库存已发生变化。 */
|
||||
public WarehouseTransferResponses.Detail submit(String customerId, String transferNo, String operatorId) {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
@@ -401,8 +448,8 @@ public class WarehouseTransferService {
|
||||
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
|
||||
? inboundSnapshots.sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
||||
.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).collect(Collectors.toSet())
|
||||
? sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
||||
.stream().map(WarehouseTransferResponses.SourceCarton::cartonMark).collect(Collectors.toSet())
|
||||
: transferCartons.listOrder(customerId, transfer.getId()).stream()
|
||||
.flatMap(carton -> java.util.stream.Stream.of(carton.getCartonMark(), carton.getCustomerCartonMark()))
|
||||
.filter(StringUtils::hasText).collect(Collectors.toSet());
|
||||
@@ -1748,6 +1795,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验换标资料及箱唛归属,退货来源同样按当前可用箱校验。 */
|
||||
private void validateRelabelRequest(String customerId, WarehouseTransferRequests.Create request) {
|
||||
if (!request.needRelabel()) {
|
||||
if (request.relabelItems() != null && !request.relabelItems().isEmpty()) {
|
||||
@@ -1757,8 +1805,8 @@ public class WarehouseTransferService {
|
||||
}
|
||||
if (request.relabelItems() == null || request.relabelItems().isEmpty()) return;
|
||||
Set<String> orderCartonMarks = StringUtils.hasText(request.inboundCode())
|
||||
? inboundSnapshots.sourceCartons(customerId, request.operatingWarehouseId().trim(), request.inboundCode().trim())
|
||||
.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark)
|
||||
? sourceCartons(customerId, request.operatingWarehouseId().trim(), request.inboundCode().trim())
|
||||
.stream().map(WarehouseTransferResponses.SourceCarton::cartonMark)
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet())
|
||||
: request.items().stream().flatMap(item -> splitMarks(item.shippingMark()).stream()).collect(Collectors.toSet());
|
||||
if (!StringUtils.hasText(request.inboundCode()) && orderCartonMarks.isEmpty()) {
|
||||
@@ -2103,6 +2151,10 @@ public class WarehouseTransferService {
|
||||
return auth == null ? null : auth.getName();
|
||||
}
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
/** 根据独立前缀识别退货来源,避免与普通入仓码混淆。 */
|
||||
private static boolean isReturnOrderSource(String value) { return StringUtils.hasText(value) && value.trim().startsWith(RETURN_ORDER_PREFIX); }
|
||||
/** 从转运来源编码提取真实退货单号。 */
|
||||
private static String returnNoFromSource(String value) { return value.trim().substring(RETURN_ORDER_PREFIX.length()).trim(); }
|
||||
private static String firstText(String first, String second) { return StringUtils.hasText(first) ? first.trim() : trim(second); }
|
||||
private static long packages(Long value) { return value == null ? 0L : value; }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper;
|
||||
import com.baiyue.lastleg.transfer.repository.TransferReturnSourceRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class TransferReturnSourceTest {
|
||||
/** 来源查询应排除占用和未清点箱,并保留整箱内的全部 SKU。 */
|
||||
@Test
|
||||
void returnsOnlyAvailableConfirmedCartonsWithContents() {
|
||||
var orders = mock(ReturnOrderMapper.class);
|
||||
var cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
var items = mock(ReturnInventoryCartonItemMapper.class);
|
||||
var repository = new TransferReturnSourceRepository();
|
||||
ReflectionTestUtils.setField(repository, "orders", orders);
|
||||
ReflectionTestUtils.setField(repository, "cartons", cartons);
|
||||
ReflectionTestUtils.setField(repository, "items", items);
|
||||
var service = new WarehouseTransferService();
|
||||
ReflectionTestUtils.setField(service, "returnSources", repository);
|
||||
var order = new ReturnOrder();
|
||||
order.setOverseasWarehouseId("WH-1");
|
||||
when(orders.findNo("C-1", "RET-1")).thenReturn(order);
|
||||
var available = carton(1L, "AVAILABLE", "CONFIRMED");
|
||||
when(cartons.currentReturnCartons("C-1", "WH-1", "RET-1")).thenReturn(List.of(
|
||||
available, carton(2L, "OUTBOUND_PENDING", "CONFIRMED"), carton(3L, "AVAILABLE", "PENDING")));
|
||||
var sku = new ReturnInventoryCartonItem();
|
||||
sku.setCartonId(1L); sku.setSkuCode("SKU-1"); sku.setQuantity(3L);
|
||||
when(items.listCartons(List.of(1L))).thenReturn(List.of(sku));
|
||||
|
||||
var result = service.sourceCartons("C-1", "WH-1", "RETURN_ORDER:RET-1");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("BOX-1", result.get(0).cartonMark());
|
||||
assertEquals("SKU-1", result.get(0).items().get(0).skuCode());
|
||||
assertEquals(3L, result.get(0).items().get(0).quantity());
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.sourceCartons("C-1", "OTHER-WH", "RETURN_ORDER:RET-1"));
|
||||
verify(cartons, never()).currentReturnCartons("C-1", "OTHER-WH", "RET-1");
|
||||
}
|
||||
|
||||
/** 构造不同库存状态的退货箱以验证来源筛选边界。 */
|
||||
private static ReturnInventoryCarton carton(long id, String status, String countStatus) {
|
||||
var carton = new ReturnInventoryCarton();
|
||||
carton.setId(id); carton.setCartonMark("BOX-" + id); carton.setTotalQuantity(3L);
|
||||
carton.setStatus(status); carton.setCountStatus(countStatus);
|
||||
return carton;
|
||||
}
|
||||
}
|
||||
+2
@@ -490,6 +490,8 @@ class WarehouseTransferServiceTest {
|
||||
@Test
|
||||
void submittingOldDraftRechecksRelabelCartonOwnership() {
|
||||
var transfer = transfer(WarehouseTransferService.DRAFT);
|
||||
transfer.setOperatingWarehouseId("WH-1");
|
||||
transfer.setInboundCode("IN-1");
|
||||
transfer.setNeedRelabel(true);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
var binding = new com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding();
|
||||
|
||||
Reference in New Issue
Block a user