merge: retain current overseas inventory fixes with looseboard demand
This commit is contained in:
+23
-7
@@ -13,6 +13,15 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface CargoDiscardInventoryMapper {
|
||||
|
||||
// 箱唛状态可能保留入库快照;废弃必须同时存在当前客户、同托盘的真实可用库存。
|
||||
String AVAILABLE_STOCK = " AND EXISTS (SELECT 1 FROM ll_dropship_order_profile dp " +
|
||||
"JOIN ll_inventory_position pos ON pos.id=dp.inventory_position_id AND pos.customer_id=dp.customer_id " +
|
||||
"AND pos.pallet_id=dp.warehouse_pallet_id " +
|
||||
"JOIN ll_inventory_balance bal ON bal.id=pos.balance_id AND bal.customer_id=pos.customer_id " +
|
||||
"WHERE dp.customer_id=c.customer_id AND dp.order_id=c.order_id AND dp.id=c.profile_id " +
|
||||
"AND dp.status='ACTIVE' AND pos.status='OCCUPIED' AND pos.available_packages>0 " +
|
||||
"AND bal.available_packages>0) ";
|
||||
|
||||
@Select("SELECT p.* FROM ll_inventory_position p WHERE p.customer_id=#{customerId} AND p.id=#{positionId} FOR UPDATE")
|
||||
com.baiyue.lastleg.inventory.entity.InventoryPosition lockGenericPosition(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||
|
||||
@@ -50,10 +59,12 @@ public interface CargoDiscardInventoryMapper {
|
||||
int changeGenericCartonStatus(@Param("customerId") String customerId, @Param("cartonId") Long cartonId,
|
||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||
|
||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} AND c.status='AVAILABLE' ORDER BY c.profile_id,c.id")
|
||||
/** 仅列出仍有实际可用库存的箱子,排除已出库托盘的残留箱唛。 */
|
||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} AND c.status='AVAILABLE' " + AVAILABLE_STOCK + " ORDER BY c.profile_id,c.id")
|
||||
List<DropshipCarton> availableOrderCartons(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
|
||||
@Select("<script>SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
/** 提交选箱时再次核对实际可用库存,避免旧页面绕过候选过滤。 */
|
||||
@Select("<script>SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||
"AND c.status='AVAILABLE' AND c.id IN <foreach collection='cartonIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"AND EXISTS(SELECT 1 FROM ll_dropship_carton_sku cs WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) ORDER BY c.profile_id,c.id FOR UPDATE</script>")
|
||||
List<DropshipCarton> lockSelected(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@@ -72,6 +83,7 @@ public interface CargoDiscardInventoryMapper {
|
||||
@Select("SELECT DISTINCT carton_mark FROM ll_inventory_ledger WHERE customer_id=#{customerId} AND position_id=#{positionId} " +
|
||||
"AND business_type IN ('STOCK_IN','RETURN_STOCK_IN') AND carton_mark IS NOT NULL AND carton_mark<>'' ORDER BY carton_mark")
|
||||
List<String> looseCartonMarks(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||
/** 按当前可用仓位筛选入库单,不能只依赖箱唛的入库状态。 */
|
||||
@Select("<script>SELECT o.id order_id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id," +
|
||||
"o.overseas_warehouse_name,o.created_at,p.id profile_id,s.id sku_inventory_id,s.sku_code,s.product_name," +
|
||||
"COUNT(c.id) available_packages FROM ll_order o " +
|
||||
@@ -80,33 +92,37 @@ public interface CargoDiscardInventoryMapper {
|
||||
"JOIN ll_dropship_carton c ON c.customer_id=o.customer_id AND c.order_id=o.id " +
|
||||
"AND c.profile_id=p.id AND c.status='AVAILABLE' " +
|
||||
"JOIN ll_dropship_carton_sku cs ON cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=s.sku_code " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.order_type='ONE_CLICK_DROPSHIP' " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.order_type='ONE_CLICK_DROPSHIP' " + AVAILABLE_STOCK +
|
||||
"<if test='inboundCode != null and inboundCode != \"\"'>AND o.inbound_code LIKE CONCAT('%',#{inboundCode},'%') </if>" +
|
||||
"GROUP BY o.id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id,o.overseas_warehouse_name,o.created_at," +
|
||||
"p.id,s.id,s.sku_code,s.product_name HAVING COUNT(c.id)>0 ORDER BY o.created_at DESC,o.id DESC,p.id,s.sku_code LIMIT 500</script>")
|
||||
List<CargoDiscardCandidateRow> candidates(@Param("customerId") String customerId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
/** SKU 可选数量仅统计实际仍在可用仓位的箱子。 */
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
||||
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
|
||||
long countAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
/** 托盘 SKU 数量与候选及提交使用相同库存门禁。 */
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||
"AND c.profile_id=#{profileId} AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
||||
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode})")
|
||||
long countAvailableProfile(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("profileId") Long profileId, @Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
/** 全部废弃锁箱时排除已出库或无实际可用库存的托盘。 */
|
||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " + AVAILABLE_STOCK +
|
||||
"AND c.status='AVAILABLE' AND EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs " +
|
||||
"WHERE cs.customer_id=c.customer_id AND cs.carton_id=c.id AND cs.sku_code=#{skuCode}) " +
|
||||
"ORDER BY c.profile_id,c.carton_sequence,c.id LIMIT #{limit} FOR UPDATE")
|
||||
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("skuCode") String skuCode, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND status='AVAILABLE'")
|
||||
/** 入库单按实际可用托盘统计物理箱数,不重复累计混装 SKU。 */
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} AND c.status='AVAILABLE' " + AVAILABLE_STOCK)
|
||||
long countAvailableOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
|
||||
@Update("UPDATE ll_dropship_carton SET status=#{toStatus},version=version+1,updated_at=NOW(3) " +
|
||||
|
||||
+10
@@ -6,6 +6,16 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RelabelItemMapper extends BaseMapper<RelabelItem> {
|
||||
/** 批量查询退货箱已完成的换标事实,避免逐箱查询。 */
|
||||
@org.apache.ibatis.annotations.Select("<script>SELECT DISTINCT i.return_carton_id FROM ll_relabel_item i " +
|
||||
"JOIN ll_relabel_application a ON a.id=i.application_id AND a.customer_id=i.customer_id " +
|
||||
"WHERE i.customer_id=#{customerId} AND i.return_carton_id IN " +
|
||||
"<foreach collection='cartonIds' item='cartonId' open='(' separator=',' close=')'>#{cartonId}</foreach> " +
|
||||
"AND a.status NOT IN ('CANCELLED','REJECTED') AND a.customer_confirmed_at IS NOT NULL " +
|
||||
"AND EXISTS(SELECT 1 FROM ll_relabel_pda_unit_fact f WHERE f.item_id=i.id AND f.result='RELABELLED')</script>")
|
||||
java.util.List<Long> completedReturnCartonIds(@org.apache.ibatis.annotations.Param("customerId") String customerId,
|
||||
@org.apache.ibatis.annotations.Param("cartonIds") java.util.List<Long> cartonIds);
|
||||
|
||||
@org.apache.ibatis.annotations.Select("SELECT ri.* FROM ll_relabel_item ri JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
|
||||
"WHERE ri.customer_id=#{customerId} AND ri.return_carton_id=#{cartonId} AND ri.application_id<>#{currentApplicationId} " +
|
||||
"AND ra.status NOT IN ('CANCELLED','REJECTED') AND ra.customer_confirmed_at IS NOT NULL " +
|
||||
|
||||
+22
@@ -5,11 +5,22 @@ import org.apache.ibatis.annotations.Select;
|
||||
@Mapper public interface InboundSnapshotMapper {
|
||||
record SourceCarton(String cartonMark, String skuCode, String productNameZh, Long quantity,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, String skuItemsJson) {}
|
||||
/** 查询来源单当前仍可转运的箱级明细,排除已经存在有效换标记录的原箱。 */
|
||||
@Select("""
|
||||
SELECT c.carton_mark cartonMark,c.sku_code skuCode,NULL productNameZh,NULL quantity,
|
||||
c.destination_warehouse_code destinationWarehouseCode,c.destination_warehouse_name destinationWarehouseName,NULL skuItemsJson
|
||||
FROM ll_inbound_plan_carton c JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND o.overseas_warehouse_id=#{warehouseId}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ll_relabel_item ri
|
||||
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||
AND UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||
AND ra.status NOT IN ('CANCELLED','REJECTED')
|
||||
)
|
||||
|
||||
|
||||
UNION ALL
|
||||
SELECT c.carton_mark,i.sku_code,i.product_name_zh,c.counted_quantity,
|
||||
t.destination_warehouse_code,t.destination_warehouse_name,c.sku_items_json
|
||||
@@ -18,6 +29,17 @@ import org.apache.ibatis.annotations.Select;
|
||||
JOIN ll_order o ON o.id=t.lastleg_order_id AND o.customer_id=t.customer_id
|
||||
JOIN ll_warehouse_transfer_item i ON i.id=c.transfer_item_id AND i.customer_id=c.customer_id AND i.transfer_order_id=t.id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ll_relabel_item ri
|
||||
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||
AND (UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||
OR UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(NULLIF(c.customer_carton_mark,''))))
|
||||
AND ra.status NOT IN ('CANCELLED','REJECTED')
|
||||
)
|
||||
|
||||
|
||||
ORDER BY cartonMark
|
||||
""")
|
||||
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
/** 使用提交单据的原箱快照表达业务占用,不提前扣减实物库存。 */
|
||||
@Mapper
|
||||
public interface TransferSourceReservationMapper {
|
||||
/** 按同一入仓快照串行提交,防止不同草稿同时抢占原箱。 */
|
||||
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND id=#{inboundOrderId} FOR UPDATE")
|
||||
Long lockInbound(@Param("customerId") String customerId, @Param("inboundOrderId") Long inboundOrderId);
|
||||
|
||||
/** 读取已提交转运的原箱;驳回、取消和草稿不占用,可重新选择。 */
|
||||
@Select("SELECT COALESCE(NULLIF(c.customer_carton_mark,''), c.carton_mark) FROM ll_warehouse_transfer_carton c "
|
||||
+ "JOIN ll_warehouse_transfer_order t ON t.id=c.transfer_order_id AND t.customer_id=c.customer_id "
|
||||
+ "WHERE t.customer_id=#{customerId} AND t.operating_warehouse_id=#{warehouseId} "
|
||||
+ "AND t.inbound_code=#{sourceCode} AND t.transfer_no <> t.inbound_code "
|
||||
+ "AND t.status NOT IN ('DRAFT','REJECTED','CANCELLED') "
|
||||
+ "AND COALESCE(NULLIF(c.customer_carton_mark,''), c.carton_mark) IS NOT NULL")
|
||||
List<String> occupiedMarks(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId, @Param("sourceCode") String sourceCode);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.transfer.repository;
|
||||
|
||||
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 封装库存转运来源箱查询,SQL和客户隔离由Mapper统一执行。 */
|
||||
@Repository
|
||||
public class TransferInventorySourceRepository {
|
||||
@Resource private InboundSnapshotMapper snapshots;
|
||||
@Resource private com.baiyue.lastleg.transfer.mapper.TransferSourceReservationMapper reservations;
|
||||
|
||||
/** 同一来源入仓单的所有库存转运先取相同的行锁。 */
|
||||
public Long lockSource(String customerId, Long inboundOrderId) {
|
||||
return reservations.lockInbound(customerId, inboundOrderId);
|
||||
}
|
||||
|
||||
/** 读取当前客户及操作仓中已提交转运的原箱快照。 */
|
||||
public List<String> occupiedMarks(String customerId, String warehouseId, String sourceCode) {
|
||||
return reservations.occupiedMarks(customerId, warehouseId, sourceCode);
|
||||
}
|
||||
|
||||
/** 读取当前来源单未被有效换标申请使用的箱级快照。 */
|
||||
public List<InboundSnapshotMapper.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
return snapshots.sourceCartons(customerId, warehouseId, inboundCode);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -6,6 +6,7 @@ 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.relabel.mapper.RelabelItemMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -17,6 +18,7 @@ public class TransferReturnSourceRepository {
|
||||
@Resource private ReturnOrderMapper orders;
|
||||
@Resource private ReturnInventoryCartonMapper cartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper items;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
|
||||
/** 按当前客户和退货单号查询来源单据。 */
|
||||
public ReturnOrder findOrder(String customerId, String returnNo) {
|
||||
@@ -25,10 +27,15 @@ public class TransferReturnSourceRepository {
|
||||
|
||||
/** 查询当前仓内已完成清点且可用的退货箱,不影响其他页面的历史库存查询。 */
|
||||
public List<ReturnInventoryCarton> availableCartons(String customerId, String warehouseId, String returnNo) {
|
||||
return cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
||||
var available = cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream()
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus())
|
||||
&& carton.getRelabelApplicationId() == null)
|
||||
.toList();
|
||||
if (available.isEmpty()) return available;
|
||||
// 已完成换标的箱虽然释放了当前占用字段,但不能再次作为原箱来源转运。
|
||||
var completed = new java.util.HashSet<>(relabelItems.completedReturnCartonIds(customerId,
|
||||
available.stream().map(ReturnInventoryCarton::getId).toList()));
|
||||
return available.stream().filter(carton -> !completed.contains(carton.getId())).toList();
|
||||
}
|
||||
|
||||
/** 批量读取已通过客户及仓库隔离校验的来源箱明细。 */
|
||||
|
||||
+51
-6
@@ -149,6 +149,7 @@ public class WarehouseTransferService {
|
||||
@Resource private InventoryCommandMapper inventoryCommands;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private TransferReturnSourceRepository returnSources;
|
||||
@Resource private com.baiyue.lastleg.transfer.repository.TransferInventorySourceRepository inventorySources;
|
||||
|
||||
/** 查询来源单据当前可用的箱唛及箱内 SKU,兼容普通入仓与退货库存。 */
|
||||
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
@@ -164,22 +165,36 @@ public class WarehouseTransferService {
|
||||
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 -> {
|
||||
return unreservedSourceCartons(customerId, warehouseId.trim(), source, 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();
|
||||
}).toList());
|
||||
}
|
||||
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source).stream().map(row -> {
|
||||
var sourceRows = inventorySources != null
|
||||
? inventorySources.sourceCartons(customerId, warehouseId.trim(), source)
|
||||
: inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source);
|
||||
return unreservedSourceCartons(customerId, warehouseId.trim(), source, sourceRows.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()));
|
||||
return new WarehouseTransferResponses.SourceCarton(row.cartonMark(),row.productNameZh(),row.quantity(),
|
||||
row.destinationWarehouseCode(),row.destinationWarehouseName(),contents);
|
||||
}).toList();
|
||||
}).toList());
|
||||
}
|
||||
|
||||
/** 待审核即按原箱快照占用;所有来源查询复用此过滤,避免重复提交同一实物箱。 */
|
||||
private List<WarehouseTransferResponses.SourceCarton> unreservedSourceCartons(String customerId,
|
||||
String warehouseId, String source, List<WarehouseTransferResponses.SourceCarton> cartons) {
|
||||
if (inventorySources == null) return cartons;
|
||||
Set<String> occupied = inventorySources.occupiedMarks(customerId, warehouseId, source).stream()
|
||||
.filter(StringUtils::hasText).map(mark -> mark.trim().toUpperCase(java.util.Locale.ROOT))
|
||||
.collect(Collectors.toSet());
|
||||
return cartons.stream().filter(box -> !occupied.contains(box.cartonMark().trim()
|
||||
.toUpperCase(java.util.Locale.ROOT))).toList();
|
||||
}
|
||||
private static LocalDateTime arrivalTime(WarehouseTransferRequests.Create request) {
|
||||
return StringUtils.hasText(request.inboundCode()) ? null : request.estimatedArrivalAt();
|
||||
@@ -334,7 +349,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
|
||||
/** Platform-created transfers keep the existing create-and-submit behavior. */
|
||||
@Transactional
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public WarehouseTransferResponses.Detail createAndSubmit(String customerId, String operatorId,
|
||||
WarehouseTransferRequests.Create request) {
|
||||
WarehouseTransferResponses.Detail draft = create(customerId, operatorId, request);
|
||||
@@ -456,7 +471,7 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
/** 提交草稿前重新校验来源箱归属,防止草稿保存后库存已发生变化。 */
|
||||
public WarehouseTransferResponses.Detail submit(String customerId, String transferNo, String operatorId) {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
@@ -464,6 +479,13 @@ public class WarehouseTransferService {
|
||||
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
||||
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
|
||||
|
||||
if (!transfer.getTransferNo().equals(transfer.getInboundCode())) {
|
||||
// 所有同源提交锁定同一入仓记录;取锁后按最新可用箱校验并保存逐箱占用快照。
|
||||
if (inventorySources != null && inventorySources.lockSource(customerId, transfer.getInboundOrderId()) == null)
|
||||
throw conflict("库存来源已失效,请重新选择来源单");
|
||||
if (inventorySources != null) reserveSourceCartons(customerId, transfer);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
|
||||
? sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
||||
@@ -500,6 +522,29 @@ public class WarehouseTransferService {
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
/** 提交时逐箱核对来源快照并记录占用,禁止并发提交时静默替换用户选择。 */
|
||||
private void reserveSourceCartons(String customerId, WarehouseTransferOrder transfer) {
|
||||
var available = new java.util.LinkedHashMap<String, String>();
|
||||
for (var box : sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())) {
|
||||
available.put(box.cartonMark().trim().toUpperCase(java.util.Locale.ROOT), box.cartonMark());
|
||||
}
|
||||
var selected = transferCartons.listOrder(customerId, transfer.getId());
|
||||
if (selected.size() != transfer.getPackageCount() || selected.isEmpty())
|
||||
throw conflict("转运箱数与来源箱明细不一致,请刷新后重新提交");
|
||||
var reserved = new java.util.LinkedHashMap<Long, String>();
|
||||
for (var carton : selected) {
|
||||
String mark = StringUtils.hasText(carton.getCustomerCartonMark())
|
||||
? carton.getCustomerCartonMark() : carton.getCartonMark();
|
||||
if (!StringUtils.hasText(mark)) throw conflict("转运箱缺少来源箱号,请刷新来源单");
|
||||
String canonical = available.remove(mark.trim().toUpperCase(java.util.Locale.ROOT));
|
||||
if (canonical == null) throw conflict("来源箱已被提交或不可用,请刷新来源单:" + mark);
|
||||
reserved.put(carton.getId(), canonical);
|
||||
}
|
||||
for (var carton : selected) {
|
||||
carton.setCustomerCartonMark(reserved.get(carton.getId()));
|
||||
transferCartons.updateById(carton);
|
||||
}
|
||||
}
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<WarehouseTransferResponses.Summary> page(String customerId, WarehouseTransferRequests.Query query) {
|
||||
return page(customerId, query, true);
|
||||
|
||||
Reference in New Issue
Block a user