fix: 按物理箱贯通一件代发履约箱数
This commit is contained in:
File diff suppressed because one or more lines are too long
+44
-12
@@ -21,18 +21,44 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
|
||||
+ "ON b.id=p.container_booking_id WHERE p.id=#{loadingPlanId}")
|
||||
LoadingPlanSnapshot loadingPlan(@Param("loadingPlanId") long loadingPlanId);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(package_count),0) FROM fl_loading_plan_item "
|
||||
+ "WHERE loading_plan_id=#{loadingPlanId} AND account_id=#{tenantId} "
|
||||
+ "AND inbound_id=#{inboundId} AND deleted=0")
|
||||
@Select("""
|
||||
SELECT COALESCE(SUM(CASE WHEN item.pallet_id IS NULL THEN item.package_count ELSE 0 END),0)
|
||||
+ COALESCE((SELECT COUNT(DISTINCT carton.id)
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
WHERE carton.tenant_id=#{tenantId} AND carton.inbound_id=#{inboundId}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=#{loadingPlanId}
|
||||
AND pallet_item.account_id=#{tenantId}
|
||||
AND pallet_item.inbound_id=#{inboundId}
|
||||
AND pallet_item.pallet_id=carton.pallet_id
|
||||
AND pallet_item.deleted=0)),0)
|
||||
FROM fl_loading_plan_item item
|
||||
WHERE item.loading_plan_id=#{loadingPlanId} AND item.account_id=#{tenantId}
|
||||
AND item.inbound_id=#{inboundId} AND item.deleted=0
|
||||
""")
|
||||
int allocatedPackages(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("loadingPlanId") long loadingPlanId);
|
||||
|
||||
@Select("SELECT item.account_id tenantId,item.inbound_id inboundId,"
|
||||
+ "COALESCE(SUM(item.package_count),0) allocatedPackageCount,MAX(inbound.process_status) processStatus "
|
||||
+ "FROM fl_loading_plan_item item JOIN fl_inbound_order inbound "
|
||||
+ "ON inbound.tenant_id=item.account_id AND inbound.id=item.inbound_id AND inbound.deleted=0 "
|
||||
+ "WHERE item.loading_plan_id=#{loadingPlanId} AND item.deleted=0 "
|
||||
+ "GROUP BY item.account_id,item.inbound_id HAVING SUM(item.package_count)>0")
|
||||
@Select("""
|
||||
SELECT item.account_id tenantId,item.inbound_id inboundId,
|
||||
COALESCE(SUM(CASE WHEN item.pallet_id IS NULL THEN item.package_count ELSE 0 END),0)
|
||||
+ COALESCE((SELECT COUNT(DISTINCT carton.id)
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
WHERE carton.tenant_id=item.account_id AND carton.inbound_id=item.inbound_id
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=#{loadingPlanId}
|
||||
AND pallet_item.account_id=item.account_id
|
||||
AND pallet_item.inbound_id=item.inbound_id
|
||||
AND pallet_item.pallet_id=carton.pallet_id
|
||||
AND pallet_item.deleted=0)),0) allocatedPackageCount,
|
||||
MAX(inbound.process_status) processStatus
|
||||
FROM fl_loading_plan_item item
|
||||
JOIN fl_inbound_order inbound ON inbound.tenant_id=item.account_id
|
||||
AND inbound.id=item.inbound_id AND inbound.deleted=0
|
||||
WHERE item.loading_plan_id=#{loadingPlanId} AND item.deleted=0
|
||||
GROUP BY item.account_id,item.inbound_id
|
||||
HAVING allocatedPackageCount>0
|
||||
""")
|
||||
List<AllocationScope> allocatedScopes(@Param("loadingPlanId") long loadingPlanId);
|
||||
|
||||
@Update("UPDATE fl_preload_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName},"
|
||||
@@ -76,9 +102,15 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
|
||||
preload.version preloadVersion,link.loading_plan_id loadingPlanId,
|
||||
plan.container_booking_id containerBookingId,booking.container_no containerNo,
|
||||
plan.plan_status loadingPlanStatus,plan.version loadingPlanVersion,
|
||||
COALESCE((SELECT SUM(item.package_count) FROM fl_loading_plan_item item
|
||||
WHERE item.loading_plan_id=link.loading_plan_id AND item.account_id=link.tenant_id
|
||||
AND item.inbound_id=link.inbound_id AND item.deleted=0),0) allocatedPackageCount,
|
||||
COALESCE((SELECT SUM(CASE WHEN item.pallet_id IS NULL THEN item.package_count ELSE 0 END)
|
||||
FROM fl_loading_plan_item item WHERE item.loading_plan_id=link.loading_plan_id
|
||||
AND item.account_id=link.tenant_id AND item.inbound_id=link.inbound_id AND item.deleted=0),0)
|
||||
+ COALESCE((SELECT COUNT(DISTINCT carton.id) FROM fl_dropship_pallet_carton carton
|
||||
WHERE carton.tenant_id=link.tenant_id AND carton.inbound_id=link.inbound_id
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=link.loading_plan_id
|
||||
AND pallet_item.account_id=link.tenant_id AND pallet_item.inbound_id=link.inbound_id
|
||||
AND pallet_item.pallet_id=carton.pallet_id AND pallet_item.deleted=0)),0) allocatedPackageCount,
|
||||
link.created_at linkedAt
|
||||
FROM fl_preload_loading_plan_link link
|
||||
JOIN fl_preload_plan preload ON preload.id=link.preload_plan_id
|
||||
|
||||
@@ -83,15 +83,19 @@ public interface LoadingCompletedEventMapper {
|
||||
SELECT carton.tenant_id AS account_id,carton.inbound_id AS source_inbound_id,
|
||||
pallet.id AS pallet_id,1 AS actual_binding,
|
||||
pallet.pallet_no AS pallet_code,NULL AS allocation_no,
|
||||
COUNT(*) OVER (PARTITION BY pallet.id,carton.tenant_id,carton.inbound_id) AS allocated_package_count,
|
||||
carton.inbound_detail_id AS source_inbound_detail_id,carton.sku_code,
|
||||
(SELECT COUNT(*) FROM fl_dropship_pallet_carton physical
|
||||
WHERE physical.pallet_id=pallet.id AND physical.tenant_id=carton.tenant_id
|
||||
AND physical.inbound_id=carton.inbound_id) AS allocated_package_count,
|
||||
COALESCE(carton_sku.inbound_detail_id,carton.inbound_detail_id) AS source_inbound_detail_id,
|
||||
COALESCE(carton_sku.sku_code,carton.sku_code) AS sku_code,
|
||||
item.product_name_zh,item.package_count,item.product_quantity,carton.box_code,
|
||||
carton.actual_sku_quantity
|
||||
COALESCE(carton_sku.actual_quantity,carton.actual_sku_quantity) AS actual_sku_quantity
|
||||
FROM fl_dropship_pallet pallet
|
||||
JOIN fl_dropship_pallet_carton carton ON carton.pallet_id=pallet.id
|
||||
LEFT JOIN fl_dropship_pallet_carton_sku carton_sku ON carton_sku.carton_id=carton.id
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=#{planId}
|
||||
AND item.account_id=carton.tenant_id AND item.inbound_id=carton.inbound_id
|
||||
AND item.inbound_detail_id=carton.inbound_detail_id
|
||||
AND item.inbound_detail_id=COALESCE(carton_sku.inbound_detail_id,carton.inbound_detail_id)
|
||||
AND item.pallet_id=pallet.id AND item.deleted=0
|
||||
WHERE pallet.pallet_status='SEALED'
|
||||
ORDER BY carton.tenant_id,carton.inbound_id,pallet.id,item.inbound_detail_id,carton.id
|
||||
|
||||
@@ -83,14 +83,34 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(SUM(CASE
|
||||
SELECT COALESCE(SUM(CASE WHEN plan_item.pallet_id IS NOT NULL THEN 0
|
||||
WHEN pick_item.item_status='SKIPPED' AND pick_item.picked_package_count=0 THEN 0
|
||||
WHEN pick_item.item_status='PARTIAL' AND pick_item.picked_package_count>0
|
||||
AND pick_item.picked_package_count<=plan_item.package_count
|
||||
THEN pick_item.picked_package_count
|
||||
ELSE plan_item.package_count
|
||||
END),0) expectedCount,
|
||||
COALESCE(SUM(pick_item.picked_package_count),0) pickedCount,
|
||||
END),0)
|
||||
+ COALESCE((SELECT COUNT(DISTINCT carton.id)
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
WHERE carton.tenant_id=#{tenantId} AND carton.inbound_id=#{inboundId}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_plan
|
||||
WHERE pallet_plan.account_id=#{tenantId}
|
||||
AND pallet_plan.inbound_id=#{inboundId}
|
||||
AND pallet_plan.pallet_id=carton.pallet_id
|
||||
AND pallet_plan.deleted=0)),0) expectedCount,
|
||||
COALESCE(SUM(CASE WHEN plan_item.pallet_id IS NULL
|
||||
THEN pick_item.picked_package_count ELSE 0 END),0)
|
||||
+ COALESCE((SELECT COUNT(DISTINCT carton.id)
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
WHERE carton.tenant_id=#{tenantId} AND carton.inbound_id=#{inboundId}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item picked_plan
|
||||
JOIN fl_loading_pick_item picked_item
|
||||
ON picked_item.loading_plan_item_id=picked_plan.id
|
||||
WHERE picked_plan.account_id=#{tenantId}
|
||||
AND picked_plan.inbound_id=#{inboundId}
|
||||
AND picked_plan.pallet_id=carton.pallet_id
|
||||
AND picked_plan.deleted=0
|
||||
AND picked_item.item_status='PICKED')),0) pickedCount,
|
||||
COUNT(DISTINCT pick_task.id) taskCount,
|
||||
COUNT(DISTINCT CASE WHEN pick_task.task_status='COMPLETED' THEN pick_task.id END) completedTaskCount,
|
||||
SUM(CASE
|
||||
|
||||
@@ -88,16 +88,23 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
||||
WHEN task.task_status = 'COMPLETING' THEN 'PROCESSING'
|
||||
WHEN task.task_status = 'COMPLETION_FAILED' THEN 'COMPLETION_FAILED'
|
||||
WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED'
|
||||
WHEN SUM(CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING') THEN 1 ELSE 0 END) = 0
|
||||
WHEN COUNT(DISTINCT CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING')
|
||||
THEN CASE WHEN plan_item.pallet_id IS NULL THEN CONCAT('I:',pick_item.id)
|
||||
ELSE CONCAT('P:',plan_item.pallet_id) END END) = 0
|
||||
THEN 'AWAITING_REVIEW'
|
||||
ELSE 'IN_PROGRESS' END AS workflowStatus,
|
||||
task.version AS version, COALESCE(assignment.version, 0) AS assignmentVersion,
|
||||
COUNT(pick_item.id) AS itemCount,
|
||||
SUM(CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING') THEN 1 ELSE 0 END) AS pendingItemCount
|
||||
COUNT(DISTINCT CASE WHEN plan_item.pallet_id IS NULL THEN CONCAT('I:',pick_item.id)
|
||||
ELSE CONCAT('P:',plan_item.pallet_id) END) AS itemCount,
|
||||
COUNT(DISTINCT CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING')
|
||||
THEN CASE WHEN plan_item.pallet_id IS NULL THEN CONCAT('I:',pick_item.id)
|
||||
ELSE CONCAT('P:',plan_item.pallet_id) END END) AS pendingItemCount
|
||||
FROM fl_loading_pick_task task
|
||||
JOIN fl_loading_plan plan ON plan.id = task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id=pick_item.loading_plan_item_id
|
||||
AND plan_item.loading_plan_id=plan.id AND plan_item.deleted=0
|
||||
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id = task.id
|
||||
WHERE task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED') AND booking.record_status = 'ACTIVE'
|
||||
AND booking.warehouse_id = #{warehouseId}
|
||||
|
||||
+16
-6
@@ -175,10 +175,12 @@ public class LoadingCompletedOutboxService {
|
||||
}
|
||||
Integer frozenPalletCount = dropship(fulfillmentType) || standardLabels.isEmpty()
|
||||
? actualPalletCount(palletRows) : Integer.valueOf(standardLabels.size());
|
||||
int physicalPackageCount = dropship(fulfillmentType)
|
||||
? physicalPackageCount(palletRows) : sumInt(sorted, Row::getPackageCount);
|
||||
return new FirstLegLoadingPlanCompletedV2.Order(string(first.getSourceInboundId()),
|
||||
string(first.getRootInboundId()), string(first.getSourceOrderId()), first.getSourceOrderNo(),
|
||||
string(first.getRootOrderId()), first.getInboundCode(), string(first.getDestinationWarehouseId()),
|
||||
first.getDestinationWarehouseCode(), first.getDestinationWarehouseName(), sumInt(sorted, Row::getPackageCount),
|
||||
first.getDestinationWarehouseCode(), first.getDestinationWarehouseName(), physicalPackageCount,
|
||||
sumNullableInt(sorted, Row::getProductQuantity), frozenPalletCount,
|
||||
sum(sorted, Row::getGrossWeightKg), sum(sorted, Row::getNetWeightKg),
|
||||
sum(sorted, Row::getVolumeCbm), sum(sorted, Row::getDimensionalWeightKg), items,
|
||||
@@ -346,7 +348,7 @@ public class LoadingCompletedOutboxService {
|
||||
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
|
||||
PalletRow::getPalletCode, LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> result = new ArrayList<>();
|
||||
int actualPackages = 0;
|
||||
Set<String> actualPackages = new HashSet<>();
|
||||
for (Map.Entry<String, List<PalletRow>> pallet : byPallet.entrySet()) {
|
||||
Map<Long, List<PalletRow>> byDetail = pallet.getValue().stream().collect(
|
||||
java.util.stream.Collectors.groupingBy(PalletRow::getSourceInboundDetailId,
|
||||
@@ -365,14 +367,14 @@ public class LoadingCompletedOutboxService {
|
||||
if (quantity <= 0) throw new BizException(ResultCode.CONFLICT, "一件代发现场托盘缺少实点 SKU 数量");
|
||||
skus.add(new FirstLegLoadingPlanCompletedV2.SkuPackageSnapshot(detail.getSourceInboundDetailId().toString(), detail.getSkuCode(),
|
||||
detail.getProductNameZh(), marks.size(), quantity, marks, cartonQuantities(detailRows.getValue(), marks)));
|
||||
actualPackages += marks.size();
|
||||
marks.forEach(mark -> actualPackages.add(pallet.getKey() + '|' + mark));
|
||||
}
|
||||
PalletManifest manifest = manifests.get(pallet.getKey());
|
||||
if (manifest == null) throw new BizException(ResultCode.CONFLICT, "一件代发托盘缺少完整清单摘要");
|
||||
result.add(new FirstLegLoadingPlanCompletedV2.PalletSnapshot(pallet.getKey(), skus,
|
||||
manifest.version(), manifest.packageCount(), manifest.fragmentCount(), manifest.hash()));
|
||||
}
|
||||
if (actualPackages != sumInt(values, Row::getPackageCount)) {
|
||||
if (actualPackages.size() != physicalPackageCount(source)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "一件代发现场托盘箱唛数与实装箱数不一致");
|
||||
}
|
||||
return result;
|
||||
@@ -390,13 +392,21 @@ public class LoadingCompletedOutboxService {
|
||||
String canonical = sorted.stream().map(value -> value.getAccountId() + "|" + value.getSourceInboundId()
|
||||
+ "|" + value.getSourceInboundDetailId() + "|" + value.getSkuCode() + "|" + value.getBoxCode()
|
||||
+ "|" + value.getActualSkuQuantity()).collect(java.util.stream.Collectors.joining("\n"));
|
||||
int fragments = (int) sorted.stream().map(PalletRow::getAccountId).distinct().count();
|
||||
result.put(palletNo, new PalletManifest(1, sorted.size(), fragments,
|
||||
int fragments = (int) sorted.stream().map(value -> value.getAccountId() + ":" + value.getSourceInboundId())
|
||||
.distinct().count();
|
||||
int packages = (int) sorted.stream().map(PalletRow::getBoxCode)
|
||||
.filter(Objects::nonNull).distinct().count();
|
||||
result.put(palletNo, new PalletManifest(1, packages, fragments,
|
||||
manifestHash(canonical)));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int physicalPackageCount(List<PalletRow> rows) {
|
||||
return (int) rows.stream().map(row -> row.getPalletCode() + '|' + row.getBoxCode())
|
||||
.distinct().count();
|
||||
}
|
||||
|
||||
private String manifestHash(String canonical) {
|
||||
String value = EventPayloadHash.sha256(canonical.getBytes(StandardCharsets.UTF_8));
|
||||
return value.substring("sha256:".length());
|
||||
|
||||
@@ -961,19 +961,7 @@ public class LoadingPickService {
|
||||
.collect(Collectors.groupingBy(CartonLabelRow::getPickItemId));
|
||||
Map<Long, Long> tenantByInbound = tenantByInbound(task.getId());
|
||||
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(source, tenantByInbound);
|
||||
List<PickItem> items = source.stream().map(item -> {
|
||||
List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
|
||||
CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0);
|
||||
Long tenantId = first == null ? tenantByInbound.get(item.getInboundId()) : first.getTenantId();
|
||||
return new PickItem(item.getId(), item.getLoadingPlanItemId(), tenantId,
|
||||
item.getInboundId(),
|
||||
item.getInboundDetailId(), item.getUnitType(), item.getPalletId(), item.getPalletNo(), item.getIndivisible(),
|
||||
first == null ? null : first.getOrderNo(),
|
||||
first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(),
|
||||
cartons.stream().map(CartonLabelRow::getBoxCode).toList(),
|
||||
inventoryByInbound.getOrDefault(item.getInboundId(), List.of()), item.getPlannedPackageCount(),
|
||||
item.getPickedPackageCount(), item.getItemStatus(), item.getVersion());
|
||||
}).toList();
|
||||
List<PickItem> items = pickItems(source, cartonsByItem, tenantByInbound, inventoryByInbound);
|
||||
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
|
||||
LoadingPickReviewEntity reviewed = repository.review(task.getId());
|
||||
Assignment assignment = assigned == null ? null : new Assignment(assigned.getAssigneeId(),
|
||||
@@ -1006,6 +994,71 @@ public class LoadingPickService {
|
||||
workflowStatus, task.getVersion(), assignment, review, completionCommand, items);
|
||||
}
|
||||
|
||||
/** One sealed pallet is one physical PDA work unit even when its cartons contain several SKU snapshots. */
|
||||
private List<PickItem> pickItems(List<LoadingPickItemEntity> source,
|
||||
Map<Long, List<CartonLabelRow>> cartonsByItem,
|
||||
Map<Long, Long> tenantByInbound,
|
||||
Map<Long, List<InventoryResource>> inventoryByInbound) {
|
||||
Map<String, List<LoadingPickItemEntity>> units = source.stream().collect(Collectors.groupingBy(
|
||||
item -> "SEALED_PALLET".equals(item.getUnitType()) && item.getPalletId() != null
|
||||
? "P:" + item.getPalletId() : "I:" + item.getId(),
|
||||
LinkedHashMap::new, Collectors.toList()));
|
||||
Set<Long> palletIds = units.values().stream().map(items -> items.get(0))
|
||||
.filter(item -> "SEALED_PALLET".equals(item.getUnitType()) && item.getPalletId() != null)
|
||||
.map(LoadingPickItemEntity::getPalletId).collect(Collectors.toSet());
|
||||
Map<Long, com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletMetrics> physical =
|
||||
palletIds.isEmpty() ? Map.of() : dropshipPalletizationMapper.physicalMetricsByPalletIds(palletIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletMetrics::palletId,
|
||||
java.util.function.Function.identity()));
|
||||
List<PickItem> result = new ArrayList<>();
|
||||
for (List<LoadingPickItemEntity> members : units.values()) {
|
||||
LoadingPickItemEntity firstItem = members.get(0);
|
||||
List<CartonLabelRow> cartons = members.stream()
|
||||
.flatMap(item -> cartonsByItem.getOrDefault(item.getId(), List.of()).stream()).toList();
|
||||
CartonLabelRow firstCarton = cartons.isEmpty() ? null : cartons.get(0);
|
||||
Long tenantId = firstCarton == null ? tenantByInbound.get(firstItem.getInboundId())
|
||||
: firstCarton.getTenantId();
|
||||
List<String> boxCodes = cartons.stream().map(CartonLabelRow::getBoxCode)
|
||||
.filter(StringUtils::hasText).distinct().toList();
|
||||
List<InventoryResource> inventory = members.stream()
|
||||
.flatMap(item -> inventoryByInbound.getOrDefault(item.getInboundId(), List.of()).stream())
|
||||
.collect(Collectors.collectingAndThen(Collectors.toMap(
|
||||
resource -> resource.reservationId() == null ? "L:" + resource.locationId()
|
||||
: "R:" + resource.reservationId(),
|
||||
java.util.function.Function.identity(), (left, right) -> left, LinkedHashMap::new),
|
||||
values -> List.copyOf(values.values())));
|
||||
if (!"SEALED_PALLET".equals(firstItem.getUnitType()) || firstItem.getPalletId() == null) {
|
||||
result.add(new PickItem(firstItem.getId(), firstItem.getLoadingPlanItemId(), tenantId,
|
||||
firstItem.getInboundId(), firstItem.getInboundDetailId(), firstItem.getUnitType(),
|
||||
firstItem.getPalletId(), firstItem.getPalletNo(), firstItem.getIndivisible(),
|
||||
firstCarton == null ? null : firstCarton.getOrderNo(),
|
||||
firstCarton == null ? null : firstCarton.getInboundCode(),
|
||||
firstCarton == null ? null : firstCarton.getCargoName(), boxCodes, inventory,
|
||||
firstItem.getPlannedPackageCount(), firstItem.getPickedPackageCount(),
|
||||
firstItem.getItemStatus(), firstItem.getVersion()));
|
||||
continue;
|
||||
}
|
||||
var metrics = physical.get(firstItem.getPalletId());
|
||||
if (metrics == null || count(metrics.cartonCount()) < 1) {
|
||||
throw conflict("托盘缺少物理箱明细,不能生成PDA整托任务");
|
||||
}
|
||||
boolean picked = members.stream().allMatch(item -> "PICKED".equals(item.getItemStatus()));
|
||||
boolean pending = members.stream().allMatch(item -> "PENDING".equals(item.getItemStatus())
|
||||
&& count(item.getPickedPackageCount()) == 0);
|
||||
String status = picked ? "PICKED" : pending ? "PENDING" : "PICKING";
|
||||
int version = members.stream().map(LoadingPickItemEntity::getVersion).filter(Objects::nonNull)
|
||||
.max(Integer::compareTo).orElse(0);
|
||||
result.add(new PickItem(firstItem.getId(), firstItem.getLoadingPlanItemId(), tenantId,
|
||||
firstItem.getInboundId(), firstItem.getInboundDetailId(), "SEALED_PALLET",
|
||||
firstItem.getPalletId(), firstItem.getPalletNo(), true,
|
||||
firstCarton == null ? null : firstCarton.getOrderNo(),
|
||||
firstCarton == null ? null : firstCarton.getInboundCode(), null, boxCodes, inventory,
|
||||
metrics.cartonCount(), picked ? metrics.cartonCount() : 0, status, version));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private List<BatchLine> validateBatchLines(LoadingPickTaskEntity task, BatchRequest request) {
|
||||
long taskId = task.getId();
|
||||
List<LoadingPickItemEntity> allItems = repository.items(taskId);
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.MeasurementItemRow;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.ParentRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
@@ -50,6 +51,8 @@ public class PdaPickSettlementService {
|
||||
private OrderFulfillmentSplitClient orderSplits;
|
||||
@Resource
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
@Resource
|
||||
private DropshipPalletizationMapper dropshipPallets;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -173,13 +176,14 @@ public class PdaPickSettlementService {
|
||||
if (measurements.stream().anyMatch(item -> !Objects.equals(sourceMeasurementId, item.measurementId()))) {
|
||||
throw conflict("主入库单存在多个已审核测量批次,不能自动拆分");
|
||||
}
|
||||
Map<Long, Integer> pickedByDetail = pickedByDetail(taskItems, parentInboundId);
|
||||
int pickedByMeasurement = pickedByDetail.values().stream().mapToInt(Integer::intValue).sum();
|
||||
if (pickedByMeasurement != pickedPackageCount) {
|
||||
throw conflict("PDA 已拣明细与入库单已拣箱数不一致");
|
||||
}
|
||||
boolean dropshipWholePallet = "ONE_CLICK_DROPSHIP".equals(
|
||||
splits.fulfillmentType(parent.tenantId(), parentInboundId));
|
||||
Map<Long, Integer> pickedByDetail = dropshipWholePallet
|
||||
? Map.of() : pickedByDetail(taskItems, parentInboundId);
|
||||
if (!dropshipWholePallet
|
||||
&& pickedByDetail.values().stream().mapToInt(Integer::intValue).sum() != pickedPackageCount) {
|
||||
throw conflict("PDA 已拣明细与入库单已拣箱数不一致");
|
||||
}
|
||||
if (dropshipWholePallet && childPackageCount != 0) {
|
||||
throw conflict("一件代发现场托盘必须整票装柜,不能生成余量分片");
|
||||
}
|
||||
@@ -192,8 +196,7 @@ public class PdaPickSettlementService {
|
||||
}
|
||||
|
||||
if (root == null || !"MASTER".equals(root.inboundRole())) throw conflict("根主入库单不存在");
|
||||
Map<Long, Integer> orderQuantities = dropshipWholePallet
|
||||
? new LinkedHashMap<>(pickedByDetail) : measurementQuantities(measurementLines);
|
||||
Map<Long, Integer> orderQuantities = measurementQuantities(measurementLines);
|
||||
OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, orderQuantities,
|
||||
pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId,
|
||||
"pda-task-" + taskId, operatorId, operatorName);
|
||||
@@ -337,11 +340,35 @@ public class PdaPickSettlementService {
|
||||
&& Objects.equals(child.containerBookingId(), containerBookingId);
|
||||
}
|
||||
|
||||
private static Map<Long, Integer> pickedByInbound(List<LoadingPickItemEntity> taskItems) {
|
||||
return taskItems.stream().filter(item -> "PICKED".equals(item.getItemStatus())
|
||||
|| "PARTIAL".equals(item.getItemStatus()))
|
||||
.collect(Collectors.groupingBy(LoadingPickItemEntity::getInboundId,
|
||||
Collectors.summingInt(item -> count(item.getPickedPackageCount(), "PDA已拣箱数"))));
|
||||
private Map<Long, Integer> pickedByInbound(List<LoadingPickItemEntity> taskItems) {
|
||||
Map<Long, Integer> result = new HashMap<>();
|
||||
Set<Long> pickedPalletIds = new java.util.LinkedHashSet<>();
|
||||
for (LoadingPickItemEntity item : taskItems) {
|
||||
if (!("PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus()))) continue;
|
||||
if ("SEALED_PALLET".equals(item.getUnitType()) && item.getPalletId() != null) {
|
||||
pickedPalletIds.add(item.getPalletId());
|
||||
} else {
|
||||
result.merge(item.getInboundId(), count(item.getPickedPackageCount(), "PDA已拣箱数"), Integer::sum);
|
||||
}
|
||||
}
|
||||
if (pickedPalletIds.isEmpty()) return result;
|
||||
Map<Long, List<LoadingPickItemEntity>> palletItems = taskItems.stream()
|
||||
.filter(item -> item.getPalletId() != null && pickedPalletIds.contains(item.getPalletId()))
|
||||
.collect(Collectors.groupingBy(LoadingPickItemEntity::getPalletId));
|
||||
if (palletItems.values().stream().anyMatch(items -> items.stream()
|
||||
.anyMatch(item -> !"PICKED".equals(item.getItemStatus())))) {
|
||||
throw conflict("一件代发整托存在未完成的 SKU 明细,不能结算物理箱数");
|
||||
}
|
||||
List<DropshipPalletizationMapper.PlanPalletMemberRow> physical =
|
||||
dropshipPallets.palletInboundMembersByPalletIds(pickedPalletIds);
|
||||
Set<Long> resolvedPallets = physical.stream().map(DropshipPalletizationMapper.PlanPalletMemberRow::getPalletId)
|
||||
.collect(Collectors.toSet());
|
||||
if (!resolvedPallets.equals(pickedPalletIds)) {
|
||||
throw conflict("一件代发托盘缺少物理箱明细,不能完成装柜结算");
|
||||
}
|
||||
physical.forEach(row -> result.merge(row.getInboundId(),
|
||||
count(row.getAvailablePackageCount(), "托盘物理箱数"), Integer::sum));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<Long, Integer> pickedByDetail(List<LoadingPickItemEntity> taskItems, long inboundId) {
|
||||
|
||||
+5
-1
@@ -13,6 +13,10 @@ class LoadingModelBridgeMapperContractTest {
|
||||
.getMethod("allocatedPackages", long.class, long.class, long.class)
|
||||
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
|
||||
|
||||
assertThat(sql).contains("inbound_id=#{inboundId} AND deleted=0");
|
||||
assertThat(sql)
|
||||
.contains("item.inbound_id=#{inboundId} AND item.deleted=0")
|
||||
.contains("COUNT(DISTINCT carton.id)")
|
||||
.contains("pallet_item.pallet_id=carton.pallet_id")
|
||||
.contains("pallet_item.deleted=0");
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -300,6 +300,8 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(actualPalletsSql)
|
||||
.contains("pallet.pallet_no AS pallet_code")
|
||||
.contains("item.pallet_id=pallet.id")
|
||||
.contains("LEFT JOIN fl_dropship_pallet_carton_sku carton_sku")
|
||||
.contains("COALESCE(carton_sku.inbound_detail_id,carton.inbound_detail_id)")
|
||||
.doesNotContain("pallet.pallet_code");
|
||||
assertThat(workPalletCountSql).contains("i.pallet_id=p.id");
|
||||
assertThat(openWorkPalletCountSql).contains("i.pallet_id=p.id");
|
||||
@@ -374,6 +376,33 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(event.payload().at("/orders/0/palletCount").asInt()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiSkuCartonsKeepSkuSnapshotsButCountPhysicalPackagesOnce() throws Exception {
|
||||
Row first = row(11, 100, 101); first.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
first.setPackageCount(2); first.setProductQuantity(20); first.setSkuCode("SKU-A");
|
||||
Row second = row(11, 100, 102); second.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
second.setPackageCount(2); second.setProductQuantity(10); second.setSkuCode("SKU-B");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(first, second));
|
||||
when(snapshots.workPalletCount(90L)).thenReturn(1);
|
||||
when(snapshots.actualPalletRows(90L)).thenReturn(List.of(
|
||||
actualPallet(11,100,"DP-260916-0009",101,"SKU-A","BOX-001",10),
|
||||
actualPallet(11,100,"DP-260916-0009",102,"SKU-B","BOX-001",5),
|
||||
actualPallet(11,100,"DP-260916-0009",101,"SKU-A","BOX-002",10),
|
||||
actualPallet(11,100,"DP-260916-0009",102,"SKU-B","BOX-002",5)));
|
||||
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.payload().at("/orders/0/packageCount").asInt()).isEqualTo(2);
|
||||
assertThat(event.payload().at("/orders/0/items")).hasSize(2);
|
||||
assertThat(event.payload().at("/orders/0/pallets/0/manifestPackageCount").asInt()).isEqualTo(2);
|
||||
assertThat(event.payload().at("/orders/0/pallets/0/skuPackages")).hasSize(2);
|
||||
assertThat(event.payload().at("/orders/0/pallets/0/skuPackages/0/cartonMarks")).hasSize(2);
|
||||
assertThat(event.payload().at("/orders/0/pallets/0/skuPackages/1/cartonMarks")).hasSize(2);
|
||||
}
|
||||
|
||||
@Test void publishesExactPdaCartonToPalletBindingsWithoutSequentialRedistribution() throws Exception {
|
||||
Row first = row(11, 100, 101); first.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
first.setPackageCount(2); first.setProductQuantity(20); first.setSkuCode("SKU-A");
|
||||
|
||||
@@ -155,6 +155,9 @@ class LoadingPickServiceTest {
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||
when(repository.inbound(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||
new LoadingInboundMapper.Snapshot(invocation.getArgument(1), invocation.getArgument(0), "LOADING", 1));
|
||||
when(dropshipPallets.physicalMetricsByPalletIds(any())).thenReturn(List.of(
|
||||
new DropshipPalletizationMapper.PalletMetrics(801L, 2,
|
||||
java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -482,6 +485,39 @@ class LoadingPickServiceTest {
|
||||
verify(completionCommands, never()).latest(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskDetailCollapsesMultiSkuRowsIntoOnePhysicalPalletCard() {
|
||||
LoadingPickItemEntity first = item();
|
||||
LoadingPickItemEntity second = item();
|
||||
second.setId(31L);
|
||||
second.setLoadingPlanItemId(41L);
|
||||
second.setInboundDetailId(61L);
|
||||
for (LoadingPickItemEntity item : List.of(first, second)) {
|
||||
item.setPlannedPackageCount(11);
|
||||
item.setUnitType("SEALED_PALLET");
|
||||
item.setPalletId(801L);
|
||||
item.setPalletNo("DP-260916-0009");
|
||||
item.setIndivisible(true);
|
||||
}
|
||||
when(dropshipPallets.physicalMetricsByPalletIds(any())).thenReturn(List.of(
|
||||
new DropshipPalletizationMapper.PalletMetrics(801L, 11,
|
||||
java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO)));
|
||||
when(repository.task(20L)).thenReturn(task());
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(first, second));
|
||||
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||
when(palletAllocations.list(101L, 50L)).thenReturn(List.of(allocation(50L, 20)));
|
||||
|
||||
Task result = service.task(20L, 101L);
|
||||
|
||||
assertThat(result.items()).singleElement().satisfies(item -> {
|
||||
assertThat(item.palletNo()).isEqualTo("DP-260916-0009");
|
||||
assertThat(item.plannedPackageCount()).isEqualTo(11);
|
||||
assertThat(item.pickedPackageCount()).isZero();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadingTaskDetailDoesNotDependOnCompletionCommandStorage() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
|
||||
+40
-5
@@ -9,6 +9,7 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.MeasurementItemRow;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.ParentRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -44,6 +45,7 @@ class PdaPickSettlementServiceTest {
|
||||
@MockBean private WarehousePalletLocationClient warehouse;
|
||||
@MockBean private OrderFulfillmentSplitClient orderSplits;
|
||||
@MockBean private InboundHoldGuard inboundHoldGuard;
|
||||
@MockBean private DropshipPalletizationMapper dropshipPallets;
|
||||
@Resource private PdaPickSettlementService service;
|
||||
|
||||
@Test
|
||||
@@ -205,7 +207,7 @@ class PdaPickSettlementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void settlesWholeDropshipPalletWithSkuFactsInsteadOfAggregateMeasurementDetail() {
|
||||
void settlesWholeDropshipUsingUnifiedPhysicalCartonMeasurement() {
|
||||
List<LoadingPickItemEntity> items = List.of(
|
||||
item(11L, 21L, 31L, 41L, 1, 1, "PICKED"),
|
||||
item(12L, 22L, 31L, 42L, 1, 1, "PICKED"),
|
||||
@@ -223,14 +225,47 @@ class PdaPickSettlementServiceTest {
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
verify(orderSplits).split(eq("internal-token"), eq(9L),
|
||||
org.mockito.ArgumentMatchers.argThat(request -> request.lines().size() == 4
|
||||
&& request.lines().stream().allMatch(line -> line.packageCount() == 1)
|
||||
&& request.lines().stream().map(OrderFulfillmentSplitClient.QuantityLine::sourceDetailId)
|
||||
.collect(java.util.stream.Collectors.toSet()).equals(java.util.Set.of(41L, 42L, 43L, 44L))));
|
||||
org.mockito.ArgumentMatchers.argThat(request -> request.lines().size() == 1
|
||||
&& request.lines().get(0).sourceDetailId() == 41L
|
||||
&& request.lines().get(0).packageCount() == 4));
|
||||
verify(splits).updateMeasurementItemForLoaded(10001L, 51L, 4);
|
||||
verify(splits).markSourceLoaded(10001L, 31L, 4, 9L, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiSkuCartonsCountOnceWhenSettlingWholeDropshipPallet() {
|
||||
LoadingPickItemEntity first = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
|
||||
LoadingPickItemEntity second = item(12L, 22L, 31L, 42L, 20, 20, "PICKED");
|
||||
for (LoadingPickItemEntity item : List.of(first, second)) {
|
||||
item.setUnitType("SEALED_PALLET");
|
||||
item.setPalletId(801L);
|
||||
item.setPalletNo("DP-260916-0009");
|
||||
item.setIndivisible(true);
|
||||
}
|
||||
resolved(List.of(first, second));
|
||||
DropshipPalletizationMapper.PlanPalletMemberRow physical =
|
||||
new DropshipPalletizationMapper.PlanPalletMemberRow();
|
||||
physical.setPalletId(801L);
|
||||
physical.setAccountId(10001L);
|
||||
physical.setInboundId(31L);
|
||||
physical.setAvailablePackageCount(20);
|
||||
when(dropshipPallets.palletInboundMembersByPalletIds(any())).thenReturn(List.of(physical));
|
||||
when(splits.lockParent(31L)).thenReturn(parent(20));
|
||||
when(splits.fulfillmentType(10001L, 31L)).thenReturn("ONE_CLICK_DROPSHIP");
|
||||
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||
when(allocations.lockConfirmed(10001L, 31L))
|
||||
.thenReturn(List.of(allocation(1L, 1, 501L, 20)));
|
||||
writes(0, 20);
|
||||
|
||||
service.settle(1L, 9L, "operator");
|
||||
|
||||
verify(splits).markSourceLoaded(10001L, 31L, 20, 9L, 3);
|
||||
verify(orderSplits).split(eq("internal-token"), eq(9L),
|
||||
org.mockito.ArgumentMatchers.argThat(request -> request.loadedPackageCount() == 20
|
||||
&& request.lines().size() == 1 && request.lines().get(0).packageCount() == 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyPureSkippedInboundRestoresCandidate() {
|
||||
LoadingPickItemEntity item = item(12L, 22L, 32L, 42L, 8, 0, "SKIPPED");
|
||||
|
||||
+33
-9
@@ -42,9 +42,11 @@ import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipOutboundCandidate;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
@@ -97,6 +99,7 @@ public class DeliveryTransferService {
|
||||
@Resource private WarehouseTransferPalletMapper warehouseTransferPallets;
|
||||
@Resource private DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Resource private DropshipCartonMapper dropshipCartons;
|
||||
@Resource private DropshipCartonSkuMapper dropshipCartonSkus;
|
||||
@Resource private DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private InboundPlanPalletAllocationMapper planAllocations;
|
||||
@@ -396,6 +399,7 @@ public class DeliveryTransferService {
|
||||
String warehouseId = null;
|
||||
Map<String, Long> requestedPackages = new LinkedHashMap<>();
|
||||
Map<String, DropshipOutboundCandidate> candidates = new HashMap<>();
|
||||
Set<String> requestedMarks = new HashSet<>();
|
||||
for (DeliveryRequests.OneClickItem input : items) {
|
||||
String sku = input.sku().trim();
|
||||
DropshipOutboundCandidate candidate = dropshipProfiles.lockOutboundCandidate(customerId,
|
||||
@@ -414,8 +418,11 @@ public class DeliveryTransferService {
|
||||
if (input.cartonMarks().size() != input.packages() || new HashSet<>(input.cartonMarks()).size() != input.cartonMarks().size()) {
|
||||
throw conflict("Specified carton marks must be unique and equal the package count");
|
||||
}
|
||||
if (input.cartonMarks().stream().map(String::trim).anyMatch(mark -> !requestedMarks.add(mark))) {
|
||||
throw conflict("同一物理箱不能在一张出库单中重复选择");
|
||||
}
|
||||
List<DropshipCarton> selected = dropshipCartons.listProfile(customerId, candidate.getProfileId()).stream()
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && input.sku().trim().equals(carton.getSkuCode())
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus())
|
||||
&& input.cartonMarks().contains(carton.getCartonMark())).toList();
|
||||
if (selected.size() != input.cartonMarks().size()) throw conflict("Specified carton marks are unavailable");
|
||||
}
|
||||
@@ -426,8 +433,8 @@ public class DeliveryTransferService {
|
||||
if (requested.getValue() > value(candidate.getAvailablePackages())) {
|
||||
throw conflict("一件代发可用箱数不足:" + candidate.getPalletNo() + " / " + sku);
|
||||
}
|
||||
List<DropshipCarton> cartons = dropshipCartons.lockAvailable(customerId,
|
||||
candidate.getProfileId(), sku, requested.getValue());
|
||||
List<DropshipCarton> cartons = dropshipCartons.lockAvailablePhysical(customerId,
|
||||
candidate.getProfileId(), requested.getValue());
|
||||
if (cartons.size() != requested.getValue()) {
|
||||
throw conflict("一件代发可用箱数不足:" + candidate.getPalletNo() + " / " + sku);
|
||||
}
|
||||
@@ -560,19 +567,31 @@ public class DeliveryTransferService {
|
||||
List<DropshipCarton> cartons = !selectedMarks.isEmpty()
|
||||
? selectedMarks.stream()
|
||||
.map(mark -> dropshipCartons.findByProfileAndMark(customerId, profile.getId(), mark)).filter(Objects::nonNull)
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && item.getSkuCode().equals(carton.getSkuCode())).toList()
|
||||
: dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus())).toList()
|
||||
: dropshipCartons.lockAvailablePhysical(customerId, profile.getId(), request.packages());
|
||||
if (cartons.size() != request.packages()) throw conflict("Insufficient available one-click dropship cartons");
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":ALLOCATE", null);
|
||||
inventoryCommands.allocate(customerId, operatorId, move);
|
||||
inventoryCommands.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":PICK", null));
|
||||
inventoryCommands.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.inventoryPositionId(), request.packages(), outbound.getOutboundNo(), outbound.getOutboundNo(), null, request.idempotencyKey() + ":SHIP", null));
|
||||
long productQuantity = 0;
|
||||
Map<String, SkuShipment> shippedSkus = new LinkedHashMap<>();
|
||||
for (DropshipCarton carton : cartons) {
|
||||
if (carton.getProductQuantity() == null || carton.getProductQuantity() <= 0) throw conflict("出库箱唛缺少逐箱实收件数");
|
||||
productQuantity = Math.addExact(productQuantity, carton.getProductQuantity());
|
||||
List<DropshipCartonSku> cartonFacts = dropshipCartonSkus.listCarton(customerId, carton.getId());
|
||||
if (cartonFacts.isEmpty()) {
|
||||
shippedSkus.merge(carton.getSkuCode(), new SkuShipment(1, carton.getProductQuantity()), SkuShipment::add);
|
||||
} else {
|
||||
for (DropshipCartonSku fact : cartonFacts) {
|
||||
shippedSkus.merge(fact.getSkuCode(), new SkuShipment(1, value(fact.getProductQuantity())), SkuShipment::add);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, SkuShipment> shipment : shippedSkus.entrySet()) {
|
||||
if (dropshipSkuInventory.deduct(customerId, profile.getId(), shipment.getKey(),
|
||||
shipment.getValue().packages(), shipment.getValue().quantity()) != 1) {
|
||||
throw conflict("Insufficient one-click dropship SKU inventory: " + shipment.getKey());
|
||||
}
|
||||
}
|
||||
if (dropshipSkuInventory.deduct(customerId, profile.getId(), item.getSkuCode(), request.packages(), productQuantity) != 1) throw conflict("Insufficient one-click dropship SKU inventory");
|
||||
for (DropshipCarton carton : cartons) { carton.setStatus("SHIPPED"); carton.setOutboundOrderNo(outbound.getOutboundNo()); carton.setVersion((carton.getVersion() == null ? 0 : carton.getVersion()) + 1); carton.setUpdatedAt(LocalDateTime.now()); dropshipCartons.updateById(carton); }
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(request.inventoryPositionId()); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
item.setPickedPackageCount(value(item.getPickedPackageCount()) + request.packages().intValue()); item.setStatus(item.getPickedPackageCount() >= item.getPackageCount() ? "PICKED" : "PARTIAL_PICKED"); outboundPallets.updateById(item);
|
||||
@@ -778,7 +797,6 @@ public class DeliveryTransferService {
|
||||
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId());
|
||||
List<String> marks = profile == null ? List.of() : dropshipCartons.listProfile(customerId, profile.getId())
|
||||
.stream().filter(carton -> "AVAILABLE".equals(carton.getStatus()))
|
||||
.filter(carton -> Objects.equals(item.getSkuCode(), carton.getSkuCode()))
|
||||
.filter(carton -> !StringUtils.hasText(item.getCartonMarks()) || frozenMarks.contains(carton.getCartonMark()))
|
||||
.map(DropshipCarton::getCartonMark).filter(StringUtils::hasText).distinct().limit(available).toList();
|
||||
if (profile != null && marks.isEmpty()) continue;
|
||||
@@ -983,6 +1001,12 @@ public class DeliveryTransferService {
|
||||
}
|
||||
private record PalletPickPlan(Long itemId, Long positionId, long packages,
|
||||
List<String> cartonMarks, int sequence) {}
|
||||
private record SkuShipment(long packages, long quantity) {
|
||||
private SkuShipment add(SkuShipment other) {
|
||||
return new SkuShipment(Math.addExact(packages, other.packages),
|
||||
Math.addExact(quantity, other.quantity));
|
||||
}
|
||||
}
|
||||
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) {
|
||||
List<Long> palletIds = items.stream().map(DispatchItem::getWarehousePalletId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
|
||||
+16
-8
@@ -17,8 +17,9 @@ public interface CargoDiscardInventoryMapper {
|
||||
"COUNT(c.id) available_packages FROM ll_order o " +
|
||||
"JOIN ll_dropship_order_profile p ON p.customer_id=o.customer_id AND p.order_id=o.id " +
|
||||
"JOIN ll_dropship_sku_inventory s ON s.customer_id=o.customer_id AND s.order_id=o.id AND s.profile_id=p.id " +
|
||||
"LEFT 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.sku_code=s.sku_code AND c.status='AVAILABLE' " +
|
||||
"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' " +
|
||||
"<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," +
|
||||
@@ -26,21 +27,28 @@ public interface CargoDiscardInventoryMapper {
|
||||
List<CargoDiscardCandidateRow> candidates(@Param("customerId") String customerId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND sku_code=#{skuCode} AND status='AVAILABLE'")
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
"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 WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND profile_id=#{profileId} AND sku_code=#{skuCode} AND status='AVAILABLE'")
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
"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 * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND sku_code=#{skuCode} AND status='AVAILABLE' ORDER BY profile_id,carton_sequence,id LIMIT #{limit} FOR UPDATE")
|
||||
@Select("SELECT c.* FROM ll_dropship_carton c WHERE c.customer_id=#{customerId} AND c.order_id=#{orderId} " +
|
||||
"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'")
|
||||
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) " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{cartonId} AND status=#{fromStatus}")
|
||||
int changeCartonStatus(@Param("customerId") String customerId, @Param("cartonId") Long cartonId,
|
||||
|
||||
+30
-2
@@ -7,9 +7,12 @@ import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -21,6 +24,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Final inventory mutation for a warehouse-approved cargo discard. */
|
||||
@@ -28,6 +32,8 @@ import java.util.Map;
|
||||
public class CargoDiscardInventoryAdjustmentService {
|
||||
@Resource private CargoDiscardInventoryMapper inventory;
|
||||
@Resource private DropshipCartonLedgerMapper cartonLedgers;
|
||||
@Resource private DropshipCartonSkuMapper cartonSkus;
|
||||
@Resource private DropshipSkuInventoryMapper skuInventories;
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@@ -40,8 +46,25 @@ public class CargoDiscardInventoryAdjustmentService {
|
||||
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
|
||||
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
|
||||
long before = value(sku.getPackageCount());
|
||||
if (before < actualPackages || inventory.deductSkuInventory(customerId, sku.getId(), actualPackages) != 1) {
|
||||
throw conflict("托盘 SKU 库存不足: " + item.getSkuCode());
|
||||
Map<String, SkuDiscard> discardedSkus = new LinkedHashMap<>();
|
||||
for (int index = 0; index < actualPackages; index++) {
|
||||
CargoDiscardCarton link = links.get(index);
|
||||
DropshipCarton carton = inventory.lockCarton(customerId, link.getCartonId());
|
||||
if (carton == null) throw conflict("托盘预留箱不存在");
|
||||
List<DropshipCartonSku> facts = cartonSkus.listCarton(customerId, carton.getId());
|
||||
if (facts.isEmpty()) {
|
||||
discardedSkus.merge(carton.getSkuCode(), new SkuDiscard(1, value(carton.getProductQuantity())), SkuDiscard::add);
|
||||
} else {
|
||||
for (DropshipCartonSku fact : facts) {
|
||||
discardedSkus.merge(fact.getSkuCode(), new SkuDiscard(1, value(fact.getProductQuantity())), SkuDiscard::add);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, SkuDiscard> discarded : discardedSkus.entrySet()) {
|
||||
if (skuInventories.deduct(customerId, item.getProfileId(), discarded.getKey(),
|
||||
discarded.getValue().packages(), discarded.getValue().quantity()) != 1) {
|
||||
throw conflict("托盘 SKU 库存不足: " + discarded.getKey());
|
||||
}
|
||||
}
|
||||
var profile = profiles.selectById(item.getProfileId());
|
||||
var position = profile == null || profile.getInventoryPositionId() == null ? null
|
||||
@@ -120,6 +143,11 @@ public class CargoDiscardInventoryAdjustmentService {
|
||||
}
|
||||
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private record SkuDiscard(long packages, long quantity) {
|
||||
private SkuDiscard add(SkuDiscard other) {
|
||||
return new SkuDiscard(Math.addExact(packages, other.packages), Math.addExact(quantity, other.quantity));
|
||||
}
|
||||
}
|
||||
private static String remarkSuffix(String value) {
|
||||
return value == null || value.isBlank() ? "" : "; " + value.trim();
|
||||
}
|
||||
|
||||
+5
-4
@@ -75,7 +75,9 @@ public class CargoDiscardService {
|
||||
CandidateAccumulator value = grouped.computeIfAbsent(row.getOrderId(), ignored -> new CandidateAccumulator(row));
|
||||
value.add(row);
|
||||
}
|
||||
return grouped.values().stream().map(CandidateAccumulator::response).toList();
|
||||
return grouped.values().stream()
|
||||
.map(value -> value.response(inventory.countAvailableOrder(customerId, value.first.getOrderId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -396,12 +398,11 @@ public class CargoDiscardService {
|
||||
skus.computeIfAbsent(row.getSkuCode(), ignored -> new CandidateSkuAccumulator(row.getSkuCode(), row.getProductName()))
|
||||
.packages += row.getAvailablePackages() == null ? 0 : row.getAvailablePackages();
|
||||
}
|
||||
private CargoDiscardResponses.Candidate response() {
|
||||
private CargoDiscardResponses.Candidate response(long physicalPackages) {
|
||||
List<CargoDiscardResponses.CandidateSku> skuViews = skus.values().stream()
|
||||
.map(value -> new CargoDiscardResponses.CandidateSku(value.skuCode, value.productName, value.packages)).toList();
|
||||
long packages = skuViews.stream().mapToLong(CargoDiscardResponses.CandidateSku::availablePackages).sum();
|
||||
return new CargoDiscardResponses.Candidate(first.getOrderId(), first.getInboundCode(), first.getInboundOrderNo(),
|
||||
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), packages, skuViews);
|
||||
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), physicalPackages, skuViews);
|
||||
}
|
||||
}
|
||||
private static final class CandidateSkuAccumulator {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** SKU facts inside one physical dropship carton. */
|
||||
@Data
|
||||
@TableName("ll_dropship_carton_sku")
|
||||
public class DropshipCartonSku {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long profileId;
|
||||
private Long cartonId;
|
||||
private String skuCode;
|
||||
private String productName;
|
||||
private Long productQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+4
@@ -27,6 +27,10 @@ public interface DropshipCartonMapper extends BaseMapper<DropshipCarton> {
|
||||
@Param("cartonMark") String cartonMark);
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} AND sku_code=#{sku} AND status='AVAILABLE' ORDER BY carton_sequence LIMIT #{packages} FOR UPDATE")
|
||||
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("profileId") Long profileId, @Param("sku") String sku, @Param("packages") long packages);
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} AND status='AVAILABLE' ORDER BY carton_sequence LIMIT #{packages} FOR UPDATE")
|
||||
List<DropshipCarton> lockAvailablePhysical(@Param("customerId") String customerId,
|
||||
@Param("profileId") Long profileId,
|
||||
@Param("packages") long packages);
|
||||
@Update("UPDATE ll_dropship_carton SET status='AVAILABLE', outbound_order_no=NULL, updated_at=NOW(3) WHERE customer_id=#{customerId} AND outbound_order_no=#{outboundNo} AND status='SHIPPED'")
|
||||
int restoreOutbound(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
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 DropshipCartonSkuMapper extends BaseMapper<DropshipCartonSku> {
|
||||
@Select("SELECT * FROM ll_dropship_carton_sku WHERE customer_id=#{customerId} AND carton_id=#{cartonId} ORDER BY id")
|
||||
List<DropshipCartonSku> listCarton(@Param("customerId") String customerId,
|
||||
@Param("cartonId") Long cartonId);
|
||||
}
|
||||
+16
-10
@@ -10,21 +10,24 @@ import java.util.List;
|
||||
public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProfile> {
|
||||
String OUTBOUND_CANDIDATE_SELECT = "SELECT pos.id inventory_position_id,p.id profile_id," +
|
||||
"p.warehouse_pallet_id,pos.overseas_warehouse_id,b.overseas_warehouse_name," +
|
||||
"p.pallet_no,b.inbound_code,o.order_no,o.customer_id,o.customer_name,s.sku_code,s.product_name," +
|
||||
"LEAST(pos.available_packages,s.package_count,(SELECT COUNT(*) FROM ll_dropship_carton c " +
|
||||
"WHERE c.customer_id=p.customer_id AND c.profile_id=p.id AND c.sku_code=s.sku_code " +
|
||||
"p.pallet_no,b.inbound_code,o.order_no,o.customer_id,o.customer_name," +
|
||||
"(SELECT CASE WHEN COUNT(*)=1 THEN MAX(s.sku_code) ELSE 'MIXED' END FROM ll_dropship_sku_inventory s " +
|
||||
"WHERE s.customer_id=p.customer_id AND s.profile_id=p.id) sku_code," +
|
||||
"(SELECT CASE WHEN COUNT(*)=1 THEN MAX(s.product_name) ELSE '混合SKU' END FROM ll_dropship_sku_inventory s " +
|
||||
"WHERE s.customer_id=p.customer_id AND s.profile_id=p.id) product_name," +
|
||||
"LEAST(pos.available_packages,(SELECT COUNT(*) FROM ll_dropship_carton c " +
|
||||
"WHERE c.customer_id=p.customer_id AND c.profile_id=p.id " +
|
||||
"AND c.status='AVAILABLE')) available_packages FROM ll_dropship_order_profile p " +
|
||||
"JOIN ll_order o ON o.id=p.order_id AND o.customer_id=p.customer_id " +
|
||||
"AND o.order_type='ONE_CLICK_DROPSHIP' AND COALESCE(o.detained,0)=0 " +
|
||||
"JOIN ll_inventory_position pos ON pos.id=p.inventory_position_id AND pos.customer_id=p.customer_id " +
|
||||
"AND pos.pallet_id=p.warehouse_pallet_id " +
|
||||
"JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"JOIN ll_dropship_sku_inventory s ON s.customer_id=p.customer_id AND s.order_id=p.order_id " +
|
||||
"AND s.profile_id=p.id ";
|
||||
"";
|
||||
String OUTBOUND_CANDIDATE_WHERE = "WHERE (#{customerId} IS NULL OR #{customerId}='' OR p.customer_id=#{customerId}) AND p.status='ACTIVE' " +
|
||||
"AND pos.status='OCCUPIED' AND pos.available_packages>0 AND s.package_count>0 " +
|
||||
"AND pos.status='OCCUPIED' AND pos.available_packages>0 " +
|
||||
"AND EXISTS (SELECT 1 FROM ll_dropship_carton c WHERE c.customer_id=p.customer_id " +
|
||||
"AND c.profile_id=p.id AND c.sku_code=s.sku_code AND c.status='AVAILABLE') " +
|
||||
"AND c.profile_id=p.id AND c.status='AVAILABLE') " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock dl WHERE dl.customer_id=p.customer_id " +
|
||||
"AND dl.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id) AND dl.status='ACTIVE' " +
|
||||
"AND (dl.expires_at IS NULL OR dl.expires_at>CURRENT_TIMESTAMP(3))) " +
|
||||
@@ -33,7 +36,7 @@ public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProf
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_outbound_pallet_item oi JOIN ll_outbound_order oo " +
|
||||
"ON oo.id=oi.outbound_order_id AND oo.customer_id=oi.customer_id " +
|
||||
"WHERE oi.customer_id=p.customer_id AND oi.warehouse_pallet_id=p.warehouse_pallet_id " +
|
||||
"AND oi.inbound_code=b.inbound_code AND oi.sku_code=s.sku_code AND oo.status IN ('WAIT_PICK','PICKING','PICKED')) ";
|
||||
"AND oi.inbound_code=b.inbound_code AND oo.status IN ('WAIT_PICK','PICKING','PICKED')) ";
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_order_profile WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY source_pallet_no")
|
||||
List<DropshipOrderProfile> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
@@ -50,12 +53,15 @@ public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProf
|
||||
|
||||
@Select("<script>" + OUTBOUND_CANDIDATE_SELECT + OUTBOUND_CANDIDATE_WHERE +
|
||||
"<if test='warehouseId != null and warehouseId != \"\"'>AND pos.overseas_warehouse_id=#{warehouseId} </if>" +
|
||||
"ORDER BY b.overseas_warehouse_name,p.pallet_no,s.sku_code LIMIT 500</script>")
|
||||
"ORDER BY b.overseas_warehouse_name,p.pallet_no,sku_code LIMIT 500</script>")
|
||||
List<DropshipOutboundCandidate> listOutboundCandidates(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId);
|
||||
|
||||
@Select(OUTBOUND_CANDIDATE_SELECT + OUTBOUND_CANDIDATE_WHERE +
|
||||
"AND pos.id=#{positionId} AND s.sku_code=#{skuCode} LIMIT 1 FOR UPDATE")
|
||||
"AND pos.id=#{positionId} " +
|
||||
"AND #{skuCode}=(SELECT CASE WHEN COUNT(*)=1 THEN MAX(s.sku_code) ELSE 'MIXED' END " +
|
||||
"FROM ll_dropship_sku_inventory s WHERE s.customer_id=p.customer_id AND s.profile_id=p.id) " +
|
||||
"LIMIT 1 FOR UPDATE")
|
||||
DropshipOutboundCandidate lockOutboundCandidate(@Param("customerId") String customerId,
|
||||
@Param("positionId") Long positionId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
+47
-16
@@ -27,6 +27,7 @@ public class DropshipOrderService {
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private DropshipSkuInventoryMapper skuInventory;
|
||||
@Resource private DropshipCartonMapper cartons;
|
||||
@Resource private DropshipCartonSkuMapper cartonSkus;
|
||||
@Resource private DropshipCartonLedgerMapper ledger;
|
||||
|
||||
@Transactional
|
||||
@@ -34,8 +35,9 @@ public class DropshipOrderService {
|
||||
LastlegOrder order = orders.selectById(request.orderId());
|
||||
if (order == null || !request.customerId().equals(order.getCustomerId())) throw conflict("Tail order not found");
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) throw conflict("Order is not a one-click dropship order");
|
||||
long totalPackages = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
|
||||
.mapToLong(DropshipRequests.SkuPackages::packageCount).sum();
|
||||
validateMarks(request);
|
||||
long totalPackages = request.pallets().stream()
|
||||
.mapToLong(DropshipRequests.Pallet::manifestPackageCount).sum();
|
||||
long totalQuantity = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
|
||||
.mapToLong(DropshipRequests.SkuPackages::productQuantity).sum();
|
||||
if (order.getPackageCount() != null && order.getPackageCount().longValue() != totalPackages) {
|
||||
@@ -65,12 +67,11 @@ public class DropshipOrderService {
|
||||
return Math.toIntExact(totalPackages);
|
||||
}
|
||||
|
||||
validateMarks(request);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (DropshipRequests.Pallet inputPallet : request.pallets()) {
|
||||
DropshipOrderProfile profile = profile(request, order, inputPallet, now);
|
||||
profiles.insert(profile);
|
||||
int sequence = 0;
|
||||
Map<String, List<CartonSkuFact>> factsByMark = new java.util.LinkedHashMap<>();
|
||||
for (DropshipRequests.SkuPackages input : inputPallet.skuPackages()) {
|
||||
DropshipSkuInventory inventory = new DropshipSkuInventory();
|
||||
inventory.setId(IdWorker.getId()); inventory.setCustomerId(request.customerId()); inventory.setOrderId(order.getId());
|
||||
@@ -79,26 +80,43 @@ public class DropshipOrderService {
|
||||
inventory.setCreatedAt(now); inventory.setUpdatedAt(now);
|
||||
skuInventory.insert(inventory);
|
||||
for (String mark : input.cartonMarks()) {
|
||||
DropshipCarton carton = new DropshipCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
|
||||
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(null); carton.setPalletNo(null);
|
||||
carton.setSkuCode(input.skuCode()); carton.setCartonMark(mark.trim()); carton.setCartonSequence(++sequence);
|
||||
carton.setProductQuantity(input.cartonQuantities().get(mark));
|
||||
carton.setStatus("PENDING_INBOUND"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
ledger.insert(stockInFact(request, order, carton, now));
|
||||
factsByMark.computeIfAbsent(mark.trim(), ignored -> new java.util.ArrayList<>())
|
||||
.add(new CartonSkuFact(input.skuCode().trim(), input.productName(),
|
||||
input.cartonQuantities().get(mark)));
|
||||
}
|
||||
}
|
||||
int sequence = 0;
|
||||
for (Map.Entry<String, List<CartonSkuFact>> entry : factsByMark.entrySet()) {
|
||||
List<CartonSkuFact> facts = entry.getValue();
|
||||
DropshipCarton carton = new DropshipCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
|
||||
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(null); carton.setPalletNo(null);
|
||||
carton.setSkuCode(facts.size() == 1 ? facts.get(0).skuCode() : "MIXED");
|
||||
carton.setCartonMark(entry.getKey()); carton.setCartonSequence(++sequence);
|
||||
carton.setProductQuantity(facts.stream().mapToLong(CartonSkuFact::quantity).sum());
|
||||
carton.setStatus("PENDING_INBOUND"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
for (CartonSkuFact fact : facts) {
|
||||
DropshipCartonSku sku = new DropshipCartonSku();
|
||||
sku.setId(IdWorker.getId()); sku.setCustomerId(request.customerId()); sku.setOrderId(order.getId());
|
||||
sku.setProfileId(profile.getId()); sku.setCartonId(carton.getId()); sku.setSkuCode(fact.skuCode());
|
||||
sku.setProductName(fact.productName()); sku.setProductQuantity(fact.quantity());
|
||||
sku.setCreatedAt(now); sku.setUpdatedAt(now); cartonSkus.insert(sku);
|
||||
}
|
||||
ledger.insert(stockInFact(request, order, carton, now));
|
||||
}
|
||||
}
|
||||
return Math.toIntExact(totalPackages);
|
||||
}
|
||||
|
||||
private void validateMarks(DropshipRequests.Receive request) {
|
||||
Set<String> marks = new HashSet<>();
|
||||
Map<String, String> palletByMark = new java.util.HashMap<>();
|
||||
Set<String> palletNos = new HashSet<>();
|
||||
for (DropshipRequests.Pallet pallet : request.pallets()) {
|
||||
if (!palletNos.add(pallet.palletNo().trim())) throw conflict("Duplicate pallet number: " + pallet.palletNo());
|
||||
String palletNo = pallet.palletNo().trim();
|
||||
if (!palletNos.add(palletNo)) throw conflict("Duplicate pallet number: " + pallet.palletNo());
|
||||
Set<String> skus = new HashSet<>();
|
||||
Set<String> physicalMarks = new HashSet<>();
|
||||
for (DropshipRequests.SkuPackages item : pallet.skuPackages()) {
|
||||
if (item.packageCount() != item.cartonMarks().size()) {
|
||||
throw conflict("SKU packageCount must equal cartonMarks count: " + item.skuCode());
|
||||
@@ -110,10 +128,20 @@ public class DropshipOrderService {
|
||||
throw conflict("逐箱实收件数必须完整且与 SKU 汇总件数一致");
|
||||
}
|
||||
if (!skus.add(item.skuCode().trim())) throw conflict("Duplicate SKU on pallet: " + item.skuCode());
|
||||
Set<String> skuMarks = new HashSet<>();
|
||||
for (String mark : item.cartonMarks()) {
|
||||
if (!marks.add(mark.trim())) throw conflict("Duplicate carton mark: " + mark);
|
||||
String normalized = mark.trim();
|
||||
if (!skuMarks.add(normalized)) throw conflict("Duplicate carton mark for SKU: " + mark);
|
||||
String existingPallet = palletByMark.putIfAbsent(normalized, palletNo);
|
||||
if (existingPallet != null && !existingPallet.equals(palletNo)) {
|
||||
throw conflict("Carton mark belongs to multiple pallets: " + mark);
|
||||
}
|
||||
physicalMarks.add(normalized);
|
||||
}
|
||||
}
|
||||
if (physicalMarks.size() != pallet.manifestPackageCount()) {
|
||||
throw conflict("Pallet physical carton total does not equal manifest package count: " + palletNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +171,7 @@ public class DropshipOrderService {
|
||||
value(sku.getPackageCount()), value(sku.getProductQuantity())))
|
||||
.toList();
|
||||
return new DropshipPalletSnapshot(profile.getSourcePalletNo(),
|
||||
skus.stream().mapToLong(DropshipPalletSnapshot.Sku::packageCount).sum(), skus);
|
||||
value(profile.getManifestPackageCount()), skus);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -213,6 +241,9 @@ public class DropshipOrderService {
|
||||
}
|
||||
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static long value(Integer value) { return value == null ? 0L : value.longValue(); }
|
||||
|
||||
private record CartonSkuFact(String skuCode, String productName, long quantity) {}
|
||||
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
|
||||
+7
-5
@@ -120,12 +120,14 @@ public class InboundStockReceiptService {
|
||||
}
|
||||
DropshipPalletSnapshot sourcePallet = dropshipPallets.get(pallet.getSourcePalletNo());
|
||||
if (sourcePallet == null || sourcePallet.packageCount() != value(allocation.getPickedPackageCount())) {
|
||||
throw conflict("One-click dropship source pallet SKU package total does not match PDA receipt");
|
||||
}
|
||||
for (DropshipPalletSnapshot.Sku sku : sourcePallet.skus()) {
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||
new SkuSnapshot(sku.skuCode(), sku.productName(), null, sku.packageCount()), receivedAt));
|
||||
throw conflict("One-click dropship source pallet physical carton total does not match PDA receipt");
|
||||
}
|
||||
DropshipPalletSnapshot.Sku single = sourcePallet.skus().size() == 1
|
||||
? sourcePallet.skus().get(0) : null;
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||
new SkuSnapshot(single == null ? "MIXED" : single.skuCode(),
|
||||
single == null ? "混合SKU" : single.productName(), null,
|
||||
sourcePallet.packageCount()), receivedAt));
|
||||
} else {
|
||||
long allocationPackages = value(allocation.getPickedPackageCount());
|
||||
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
|
||||
|
||||
+15
-2
@@ -219,9 +219,22 @@ public class LoadingCompletedConsumer {
|
||||
FirstLegLoadingPlanCompletedV1.Order order = payload.orders().get(index);
|
||||
Set<String> orderCartonMarks = new HashSet<>();
|
||||
if ("ONE_CLICK_DROPSHIP".equals(order.orderType())) {
|
||||
for (FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet : order.pallets()) {
|
||||
JsonNode rawPallets = rawPayload.path("orders").get(index).path("pallets");
|
||||
for (int palletIndex = 0; palletIndex < order.pallets().size(); palletIndex++) {
|
||||
FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet = order.pallets().get(palletIndex);
|
||||
Set<String> palletCartonMarks = new HashSet<>();
|
||||
for (FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot sku : pallet.skuPackages()) {
|
||||
requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), orderCartonMarks);
|
||||
requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), new HashSet<>());
|
||||
palletCartonMarks.addAll(sku.cartonMarks());
|
||||
}
|
||||
int manifestPackages = rawPallets.path(palletIndex).path("manifestPackageCount").asInt(-1);
|
||||
if (palletCartonMarks.size() != manifestPackages) {
|
||||
throw new IllegalArgumentException("一件代发托盘物理箱数与清单箱数不一致: " + pallet.palletNo());
|
||||
}
|
||||
for (String mark : palletCartonMarks) {
|
||||
if (!orderCartonMarks.add(mark)) {
|
||||
throw new IllegalArgumentException("一件代发物理箱不能跨托盘重复: " + mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
dropshipReceivingLabels(order, rawPayload.path("orders").get(index));
|
||||
|
||||
@@ -1917,6 +1917,18 @@ CREATE TABLE IF NOT EXISTS ll_dropship_carton (
|
||||
KEY idx_ll_dropship_carton_status(customer_id,status), KEY idx_ll_dropship_carton_sku(customer_id,sku_code,status)
|
||||
) ENGINE=InnoDB COMMENT='一件代发箱唛明细';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_dropship_carton_sku (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', order_id BIGINT NOT NULL COMMENT '尾程订单ID',
|
||||
profile_id BIGINT NOT NULL COMMENT '一件代发托盘档案ID', carton_id BIGINT NOT NULL COMMENT '一件代发物理箱ID',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '箱内SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称快照',
|
||||
product_quantity BIGINT NOT NULL COMMENT '该物理箱内此SKU实收件数',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_dropship_carton_sku(carton_id,sku_code),
|
||||
KEY idx_ll_dropship_carton_sku_order(customer_id,order_id,sku_code),
|
||||
KEY idx_ll_dropship_carton_sku_profile(customer_id,profile_id,sku_code)
|
||||
) ENGINE=InnoDB COMMENT='一件代发物理箱内SKU明细';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_dropship_sku_inventory (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', order_id BIGINT NOT NULL COMMENT '订单ID', profile_id BIGINT NOT NULL COMMENT '托盘档案ID',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称', package_count BIGINT NOT NULL COMMENT '箱数', product_quantity BIGINT NOT NULL COMMENT '产品数量',
|
||||
|
||||
+6
-4
@@ -13,20 +13,22 @@ class DropshipOutboundCandidateMapperContractTest {
|
||||
String sql = select("listOutboundCandidates", String.class, String.class);
|
||||
for (String token : new String[] {"p.customer_id=#{customerId}", "p.status='ACTIVE'",
|
||||
"o.order_type='ONE_CLICK_DROPSHIP'", "COALESCE(o.detained,0)=0", "pos.status='OCCUPIED'",
|
||||
"pos.available_packages>0", "s.package_count>0", "c.status='AVAILABLE'",
|
||||
"pos.available_packages>0", "c.status='AVAILABLE'", "COUNT(*)=1", "ELSE 'MIXED'",
|
||||
"ll_dispatch_pallet_lock", "ll_warehouse_transfer_inventory_allocation",
|
||||
"ll_outbound_pallet_item", "pos.overseas_warehouse_id=#{warehouseId}", "LIMIT 500"}) {
|
||||
assertTrue(sql.contains(token), token);
|
||||
}
|
||||
assertTrue(sql.contains("LEAST(pos.available_packages,s.package_count"));
|
||||
assertTrue(sql.contains("LEAST(pos.available_packages,(SELECT COUNT(*) FROM ll_dropship_carton"));
|
||||
assertTrue(sql.contains("#{customerId} IS NULL OR #{customerId}='' OR p.customer_id=#{customerId}"));
|
||||
assertTrue(sql.contains("ORDER BY b.overseas_warehouse_name,p.pallet_no,sku_code"));
|
||||
assertTrue(!sql.contains("ORDER BY b.overseas_warehouse_name,p.pallet_no,s.sku_code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void creationLockUsesTheSameEligibilityContractAndLocksOneSkuDimension() throws Exception {
|
||||
void creationLockUsesTheSameEligibilityContractAndLocksOnePhysicalPalletDimension() throws Exception {
|
||||
String sql = select("lockOutboundCandidate", String.class, Long.class, String.class);
|
||||
assertTrue(sql.contains("AND pos.id=#{positionId}"));
|
||||
assertTrue(sql.contains("AND s.sku_code=#{skuCode}"));
|
||||
assertTrue(sql.contains("AND #{skuCode}=(SELECT CASE WHEN COUNT(*)=1"));
|
||||
assertTrue(sql.contains("LIMIT 1 FOR UPDATE"));
|
||||
assertTrue(sql.contains("ll_dispatch_pallet_lock"));
|
||||
assertTrue(sql.contains("ll_outbound_pallet_item"));
|
||||
|
||||
+23
-5
@@ -34,8 +34,10 @@ import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipOutboundCandidate;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -83,6 +85,7 @@ class DeliveryTransferServiceTest {
|
||||
@Mock InventoryBalanceMapper inventoryBalances;
|
||||
@Mock DropshipOrderProfileMapper dropshipProfiles;
|
||||
@Mock DropshipCartonMapper dropshipCartons;
|
||||
@Mock DropshipCartonSkuMapper dropshipCartonSkus;
|
||||
@Mock DropshipSkuInventoryMapper dropshipSkuInventory;
|
||||
@Mock WarehouseTransferPalletMapper warehouseTransferPallets;
|
||||
|
||||
@@ -112,6 +115,7 @@ class DeliveryTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "inventoryBalances", inventoryBalances);
|
||||
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
|
||||
ReflectionTestUtils.setField(service, "dropshipCartons", dropshipCartons);
|
||||
ReflectionTestUtils.setField(service, "dropshipCartonSkus", dropshipCartonSkus);
|
||||
ReflectionTestUtils.setField(service, "dropshipSkuInventory", dropshipSkuInventory);
|
||||
ReflectionTestUtils.setField(service, "warehouseTransferPallets", warehouseTransferPallets);
|
||||
}
|
||||
@@ -458,7 +462,7 @@ class DeliveryTransferServiceTest {
|
||||
for (long i = 1; i <= 2; i++) {
|
||||
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 900L + i, "SKU-1"))
|
||||
.thenReturn(dropshipCandidate(900L + i, 1000L + i, "WH-1", "DP-SHARED", "SKU-1", 1L));
|
||||
when(dropshipCartons.lockAvailable("TENANT-A", 1000L + i, "SKU-1", 1L))
|
||||
when(dropshipCartons.lockAvailablePhysical("TENANT-A", 1000L + i, 1L))
|
||||
.thenReturn(List.of(new DropshipCarton()));
|
||||
InventoryPosition position = new InventoryPosition();
|
||||
position.setId(900L + i); position.setCustomerId("TENANT-A"); position.setBalanceId(800L + i);
|
||||
@@ -489,7 +493,7 @@ class DeliveryTransferServiceTest {
|
||||
when(inventoryPositions.selectById(901L)).thenReturn(position);
|
||||
DropshipOutboundCandidate candidate = dropshipCandidate(901L, 1001L, "WH-1", "PLT-501", "MIXED", 1L);
|
||||
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 901L, "MIXED")).thenReturn(candidate);
|
||||
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "MIXED", 1L)).thenReturn(List.of());
|
||||
when(dropshipCartons.lockAvailablePhysical("TENANT-A", 1001L, 1L)).thenReturn(List.of());
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.createOneClick(
|
||||
"TENANT-A", "U-1", new DeliveryRequests.OneClickOutbound("recipient", List.of(
|
||||
@@ -807,15 +811,15 @@ class DeliveryTransferServiceTest {
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
|
||||
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
|
||||
OutboundPalletItem item = pallet(501L, "DP-1", "ONT8", "Ontario", "IN-A", 2);
|
||||
item.setSkuCode("SKU-1"); item.setCartonMarks("BOX-A,BOX-B");
|
||||
item.setSkuCode("MIXED"); item.setCartonMarks("BOX-A,BOX-B");
|
||||
InventoryPosition position = new InventoryPosition(); position.setId(901L); position.setCustomerId("TENANT-A");
|
||||
position.setBalanceId(801L); position.setPalletId(501L); position.setOverseasWarehouseId("WH-1");
|
||||
InventoryBalance balance = new InventoryBalance(); balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-A");
|
||||
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
|
||||
DropshipCarton first = new DropshipCarton(); first.setId(1101L); first.setProductQuantity(7L);
|
||||
DropshipCarton second = new DropshipCarton(); second.setId(1102L); second.setProductQuantity(13L);
|
||||
first.setStatus("AVAILABLE"); first.setSkuCode("SKU-1"); first.setCartonMark("BOX-A");
|
||||
second.setStatus("AVAILABLE"); second.setSkuCode("SKU-1"); second.setCartonMark("BOX-B");
|
||||
first.setStatus("AVAILABLE"); first.setSkuCode("MIXED"); first.setCartonMark("BOX-A");
|
||||
second.setStatus("AVAILABLE"); second.setSkuCode("MIXED"); second.setCartonMark("BOX-B");
|
||||
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
|
||||
when(outboundOrders.selectById(301L)).thenReturn(outbound);
|
||||
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
|
||||
@@ -825,8 +829,14 @@ class DeliveryTransferServiceTest {
|
||||
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
|
||||
when(dropshipCartons.findByProfileAndMark("TENANT-A", 1001L, "BOX-A")).thenReturn(first);
|
||||
when(dropshipCartons.findByProfileAndMark("TENANT-A", 1001L, "BOX-B")).thenReturn(second);
|
||||
when(dropshipCartonSkus.listCarton("TENANT-A", 1101L)).thenReturn(List.of(
|
||||
cartonSku("SKU-1", 7), cartonSku("SKU-2", 3)));
|
||||
when(dropshipCartonSkus.listCarton("TENANT-A", 1102L)).thenReturn(List.of(
|
||||
cartonSku("SKU-1", 13), cartonSku("SKU-2", 2)));
|
||||
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L)).thenReturn(1);
|
||||
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-2", 1L, 3L)).thenReturn(1);
|
||||
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L)).thenReturn(1);
|
||||
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-2", 1L, 2L)).thenReturn(1);
|
||||
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
|
||||
new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "wrong-mark", List.of("WRONG-BOX"))));
|
||||
@@ -837,7 +847,9 @@ class DeliveryTransferServiceTest {
|
||||
new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "repeat-box-new-key", List.of("BOX-A"))));
|
||||
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-13", List.of("BOX-B")));
|
||||
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L);
|
||||
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-2", 1L, 3L);
|
||||
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L);
|
||||
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-2", 1L, 2L);
|
||||
assertEquals("SHIPPED", first.getStatus()); assertEquals("SHIPPED", second.getStatus());
|
||||
assertEquals(2, item.getPickedPackageCount());
|
||||
}
|
||||
@@ -962,4 +974,10 @@ class DeliveryTransferServiceTest {
|
||||
value.setOverseasWarehouseId(warehouseId); value.setPalletNo(palletNo); value.setSkuCode(sku);
|
||||
value.setAvailablePackages(packages); return value;
|
||||
}
|
||||
|
||||
private static DropshipCartonSku cartonSku(String sku, long quantity) {
|
||||
DropshipCartonSku value = new DropshipCartonSku();
|
||||
value.setSkuCode(sku); value.setProductQuantity(quantity);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CargoDiscardPhysicalCartonMapperContractTest {
|
||||
@Test
|
||||
void candidatesJoinSkuFactsButCountAvailableOrdersPhysically() throws Exception {
|
||||
String candidates = select("candidates", String.class, String.class);
|
||||
assertTrue(candidates.contains("JOIN ll_dropship_carton_sku cs"));
|
||||
assertTrue(candidates.contains("cs.carton_id=c.id"));
|
||||
assertTrue(candidates.contains("COUNT(c.id) available_packages"));
|
||||
|
||||
String physicalTotal = select("countAvailableOrder", String.class, Long.class);
|
||||
assertTrue(physicalTotal.contains("COUNT(*) FROM ll_dropship_carton"));
|
||||
assertTrue(!physicalTotal.contains("ll_dropship_carton_sku"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skuSelectionResolvesThroughPhysicalCartonSkuFacts() throws Exception {
|
||||
String count = select("countAvailable", String.class, Long.class, String.class);
|
||||
String lock = select("lockAvailable", String.class, Long.class, String.class, int.class);
|
||||
for (String sql : new String[] {count, lock}) {
|
||||
assertTrue(sql.contains("EXISTS (SELECT 1 FROM ll_dropship_carton_sku cs"));
|
||||
assertTrue(sql.contains("cs.carton_id=c.id"));
|
||||
assertTrue(sql.contains("cs.sku_code=#{skuCode}"));
|
||||
}
|
||||
assertTrue(lock.contains("FOR UPDATE"));
|
||||
}
|
||||
|
||||
private String select(String method, Class<?>... parameterTypes) throws Exception {
|
||||
Select annotation = CargoDiscardInventoryMapper.class.getMethod(method, parameterTypes)
|
||||
.getAnnotation(Select.class);
|
||||
assertNotNull(annotation);
|
||||
return String.join("", annotation.value());
|
||||
}
|
||||
}
|
||||
+56
@@ -6,8 +6,16 @@ import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -37,6 +45,11 @@ import static org.mockito.Mockito.when;
|
||||
class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
@Mock CargoDiscardInventoryMapper inventory;
|
||||
@Mock DropshipCartonLedgerMapper ledgers;
|
||||
@Mock DropshipCartonSkuMapper cartonSkus;
|
||||
@Mock DropshipSkuInventoryMapper skuInventories;
|
||||
@Mock DropshipOrderProfileMapper profiles;
|
||||
@Mock InventoryPositionMapper positions;
|
||||
@Mock InventoryBalanceMapper balances;
|
||||
private CargoDiscardInventoryAdjustmentService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -44,6 +57,45 @@ class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
service = new CargoDiscardInventoryAdjustmentService();
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "cartonLedgers", ledgers);
|
||||
ReflectionTestUtils.setField(service, "cartonSkus", cartonSkus);
|
||||
ReflectionTestUtils.setField(service, "skuInventories", skuInventories);
|
||||
ReflectionTestUtils.setField(service, "profiles", profiles);
|
||||
ReflectionTestUtils.setField(service, "positions", positions);
|
||||
ReflectionTestUtils.setField(service, "balances", balances);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaDiscardDeductsEverySkuInsideOnePhysicalCartonOnce() {
|
||||
CargoDiscardApplication application = application(1);
|
||||
CargoDiscardItem item = item(1);
|
||||
item.setProfileId(201L);
|
||||
DropshipCarton carton = carton(1L);
|
||||
carton.setSkuCode("MIXED"); carton.setProductQuantity(5L);
|
||||
DropshipCartonSku skuA = cartonSku("SKU-A", 2L);
|
||||
DropshipCartonSku skuB = cartonSku("SKU-B", 3L);
|
||||
DropshipOrderProfile profile = new DropshipOrderProfile();
|
||||
profile.setId(201L); profile.setCustomerId("100"); profile.setInventoryPositionId(801L);
|
||||
InventoryPosition position = new InventoryPosition();
|
||||
position.setId(801L); position.setBalanceId(901L);
|
||||
when(inventory.lockSkuInventory("100", 301L)).thenReturn(sku(5));
|
||||
when(inventory.lockCarton("100", 1L)).thenReturn(carton);
|
||||
when(cartonSkus.listCarton("100", 1L)).thenReturn(List.of(skuA, skuB));
|
||||
when(skuInventories.deduct("100", 201L, "SKU-A", 1, 2)).thenReturn(1);
|
||||
when(skuInventories.deduct("100", 201L, "SKU-B", 1, 3)).thenReturn(1);
|
||||
when(profiles.selectById(201L)).thenReturn(profile);
|
||||
when(positions.findTenantPosition("100", 801L)).thenReturn(position);
|
||||
when(positions.deductAvailable("100", 801L, 1)).thenReturn(1);
|
||||
when(balances.deductAvailable("100", 901L, 1)).thenReturn(1);
|
||||
when(inventory.changeCartonStatus("100", 1L, "DISCARD_PENDING", "DISCARDED")).thenReturn(1);
|
||||
when(inventory.skuPackages("100", 301L)).thenReturn(4L);
|
||||
|
||||
var result = service.discardPalletItem("100", application, item, List.of(link(1L)),
|
||||
1, "operator", LocalDateTime.now());
|
||||
|
||||
assertEquals(1, result.discardedPackages());
|
||||
verify(skuInventories).deduct("100", 201L, "SKU-A", 1, 2);
|
||||
verify(skuInventories).deduct("100", 201L, "SKU-B", 1, 3);
|
||||
verify(positions).deductAvailable("100", 801L, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,4 +157,8 @@ class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
private DropshipCarton carton(long id) {
|
||||
DropshipCarton value = new DropshipCarton(); value.setId(id); value.setCartonMark("C-" + id); return value;
|
||||
}
|
||||
private DropshipCartonSku cartonSku(String skuCode, long quantity) {
|
||||
DropshipCartonSku value = new DropshipCartonSku();
|
||||
value.setSkuCode(skuCode); value.setProductQuantity(quantity); return value;
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -10,6 +10,7 @@ import com.baiyue.lastleg.discard.mapper.CargoDiscardCartonMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
@@ -102,6 +103,20 @@ class CargoDiscardServiceTest {
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateTotalCountsPhysicalCartonsInsteadOfSkuRows() {
|
||||
CargoDiscardCandidateRow skuA = candidate("SKU-A", 20L);
|
||||
CargoDiscardCandidateRow skuB = candidate("SKU-B", 20L);
|
||||
when(inventory.candidates("100", "RK-1")).thenReturn(List.of(skuA, skuB));
|
||||
when(inventory.countAvailableOrder("100", 101L)).thenReturn(20L);
|
||||
|
||||
var result = service.candidates("100", "RK-1");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(20, result.get(0).availablePackages());
|
||||
assertEquals(2, result.get(0).skus().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalPushesToPdaWithoutDeductingInventory() {
|
||||
CargoDiscardApplication application = pendingApplication();
|
||||
@@ -179,4 +194,12 @@ class CargoDiscardServiceTest {
|
||||
value.setCustomerId("100"); value.setCartonId(cartonId); value.setCartonMark("C-" + cartonId);
|
||||
return value;
|
||||
}
|
||||
|
||||
private CargoDiscardCandidateRow candidate(String skuCode, long packages) {
|
||||
CargoDiscardCandidateRow value = new CargoDiscardCandidateRow();
|
||||
value.setOrderId(101L); value.setInboundCode("RK-1"); value.setInboundOrderNo("IN-1");
|
||||
value.setOverseasWarehouseId("WH-1"); value.setOverseasWarehouseName("LA");
|
||||
value.setSkuCode(skuCode); value.setProductName(skuCode); value.setAvailablePackages(packages);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -5,8 +5,10 @@ import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonSku;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonSkuMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
@@ -32,6 +34,7 @@ class DropshipOrderServiceTest {
|
||||
@Mock private DropshipOrderProfileMapper profiles;
|
||||
@Mock private DropshipSkuInventoryMapper skuInventory;
|
||||
@Mock private DropshipCartonMapper cartons;
|
||||
@Mock private DropshipCartonSkuMapper cartonSkus;
|
||||
@Mock private DropshipCartonLedgerMapper ledger;
|
||||
@Mock private WarehouseResourceGateway warehouse;
|
||||
@InjectMocks private DropshipOrderService service;
|
||||
@@ -62,6 +65,7 @@ class DropshipOrderServiceTest {
|
||||
verify(profiles, times(2)).insert(any(DropshipOrderProfile.class));
|
||||
verify(skuInventory, times(2)).insert(any(DropshipSkuInventory.class));
|
||||
verify(cartons, times(3)).insert(any(DropshipCarton.class));
|
||||
verify(cartonSkus, times(3)).insert(any(DropshipCartonSku.class));
|
||||
verify(ledger, times(3)).insert(any(DropshipCartonLedger.class));
|
||||
|
||||
ArgumentCaptor<DropshipOrderProfile> profilesCaptor = ArgumentCaptor.forClass(DropshipOrderProfile.class);
|
||||
@@ -88,6 +92,33 @@ class DropshipOrderServiceTest {
|
||||
.containsOnly(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiSkuCartonsAreStoredOnceWithSeparateSkuFacts() {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(10L); order.setCustomerId("C-1"); order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
order.setOrderNo("LL-10"); order.setPackageCount(2); order.setProductQuantity(30);
|
||||
when(orders.selectById(10L)).thenReturn(order);
|
||||
when(profiles.listOrder("C-1", 10L)).thenReturn(List.of());
|
||||
|
||||
DropshipRequests.Receive request = new DropshipRequests.Receive("C-1", 10L, 20L, "EVENT-MIXED",
|
||||
List.of(new DropshipRequests.Pallet("SOURCE-P-1", List.of(
|
||||
new DropshipRequests.SkuPackages("SKU-A", "A", 2, 20,
|
||||
List.of("BOX-1", "BOX-2"), java.util.Map.of("BOX-1", 10L, "BOX-2", 10L)),
|
||||
new DropshipRequests.SkuPackages("SKU-B", "B", 2, 10,
|
||||
List.of("BOX-1", "BOX-2"), java.util.Map.of("BOX-1", 5L, "BOX-2", 5L))),
|
||||
1, 2, 1, "2".repeat(64))));
|
||||
|
||||
assertThat(service.receive(request)).isEqualTo(2);
|
||||
ArgumentCaptor<DropshipCarton> physical = ArgumentCaptor.forClass(DropshipCarton.class);
|
||||
verify(cartons, times(2)).insert(physical.capture());
|
||||
assertThat(physical.getAllValues()).extracting(DropshipCarton::getCartonMark)
|
||||
.containsExactly("BOX-1", "BOX-2");
|
||||
assertThat(physical.getAllValues()).extracting(DropshipCarton::getSkuCode).containsOnly("MIXED");
|
||||
assertThat(physical.getAllValues()).extracting(DropshipCarton::getProductQuantity).containsOnly(15L);
|
||||
verify(cartonSkus, times(4)).insert(any(DropshipCartonSku.class));
|
||||
verify(ledger, times(2)).insert(any(DropshipCartonLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activatesMultiplePalletsOnlyAfterPdaStockIn() {
|
||||
DropshipOrderProfile first = profile(1L, "SOURCE-P-1");
|
||||
|
||||
+23
@@ -245,6 +245,29 @@ class InboundStockReceiptServiceTest {
|
||||
assertEquals("WH-PALLET-21", activationReceipts.getValue().get(0).getPalletNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiSkuDropshipPalletStocksPhysicalCartonsOnce() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(
|
||||
new DropshipPalletSnapshot("PLT-21", 5, List.of(
|
||||
new DropshipPalletSnapshot.Sku("SKU-A", "A", 5, 50),
|
||||
new DropshipPalletSnapshot.Sku("SKU-B", "B", 5, 25)))));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> request =
|
||||
ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory, times(1)).stockIn(eq("100"), eq("operator"), request.capture());
|
||||
assertEquals("MIXED", request.getValue().skuCode());
|
||||
assertEquals(5, request.getValue().packages());
|
||||
verify(receipts, times(1)).insert(any(InboundStockReceipt.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDomesticSourceWarehouseAsActualOverseasWarehouse() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ class SchemaChineseCommentContractTest {
|
||||
String overseasUnified = Files.readString(
|
||||
Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
List<TableDefinition> baselineTables = assertAllChineseComments(baseline, "基线 SQL", 55);
|
||||
assertAllChineseComments(incremental, "增量 SQL", 49);
|
||||
List<TableDefinition> baselineTables = assertAllChineseComments(baseline, "基线 SQL", 56);
|
||||
assertAllChineseComments(incremental, "增量 SQL", 50);
|
||||
assertAllChineseComments(overseasUnified, "海外统一增量 SQL", 10);
|
||||
assertCommentMigrationCovers(incremental, overseasUnified, baselineTables);
|
||||
}
|
||||
|
||||
+6
-5
@@ -35,22 +35,23 @@ class LoadingCompletedConsumerTest {
|
||||
}
|
||||
@Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails(){
|
||||
var itemA=new FirstLegLoadingPlanCompletedV1.Item("d-a","SKU-A","产品A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-A","MARK-A",null,List.of("BOX-A1","BOX-A2"));
|
||||
var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",3,15,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-B1","BOX-B2","BOX-B3"));
|
||||
var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",2,10,1,BigDecimal.ZERO,BigDecimal.ZERO,BigDecimal.ZERO,BigDecimal.ZERO,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-A1","BOX-A2"));
|
||||
var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(
|
||||
new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","产品A",2,20,List.of("BOX-A1","BOX-A2")),
|
||||
new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-B","产品B",3,15,List.of("BOX-B1","BOX-B2","BOX-B3"))));
|
||||
var order=new FirstLegLoadingPlanCompletedV1.Order("in-multi","root-multi","ord-multi","FL-MULTI","root-ord-multi","RK-MULTI","wh-1","ONT8","ONT8",5,35,1,BigDecimal.valueOf(2),BigDecimal.valueOf(2),BigDecimal.valueOf(2),List.of(itemA,itemB),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA");
|
||||
new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-B","产品B",2,10,List.of("BOX-A1","BOX-A2"))));
|
||||
var order=new FirstLegLoadingPlanCompletedV1.Order("in-multi","root-multi","ord-multi","FL-MULTI","root-ord-multi","RK-MULTI","wh-1","ONT8","ONT8",2,30,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(itemA,itemB),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA");
|
||||
EventEnvelope event=eventForPayload(new FirstLegLoadingPlanCompletedV1("plan-multi","LP-MULTI","book-multi","MSCU6639870","CAB-MULTI","92","Warehouse",List.of(order)),2);
|
||||
|
||||
assertEquals(1,service.consume(event));
|
||||
verify(orders,times(1)).insert(any(LastlegOrder.class));
|
||||
ArgumentCaptor<LastlegOrderDetail> detailCaptor=ArgumentCaptor.forClass(LastlegOrderDetail.class);
|
||||
verify(details,times(2)).insert(detailCaptor.capture());
|
||||
assertEquals(Map.of("SKU-A",2,"SKU-B",3),detailCaptor.getAllValues().stream().collect(java.util.stream.Collectors.toMap(LastlegOrderDetail::getSkuCode,LastlegOrderDetail::getPackageCount)));
|
||||
assertEquals(Map.of("SKU-A",2,"SKU-B",2),detailCaptor.getAllValues().stream().collect(java.util.stream.Collectors.toMap(LastlegOrderDetail::getSkuCode,LastlegOrderDetail::getPackageCount)));
|
||||
ArgumentCaptor<DropshipRequests.Receive> receiveCaptor=ArgumentCaptor.forClass(DropshipRequests.Receive.class);
|
||||
verify(dropshipOrders).receive(receiveCaptor.capture());
|
||||
assertEquals(2,receiveCaptor.getValue().pallets().get(0).skuPackages().size());
|
||||
assertEquals(Map.of("SKU-A",2L,"SKU-B",3L),receiveCaptor.getValue().pallets().get(0).skuPackages().stream().collect(java.util.stream.Collectors.toMap(DropshipRequests.SkuPackages::skuCode,DropshipRequests.SkuPackages::packageCount)));
|
||||
assertEquals(2,receiveCaptor.getValue().pallets().get(0).manifestPackageCount());
|
||||
assertEquals(Map.of("SKU-A",2L,"SKU-B",2L),receiveCaptor.getValue().pallets().get(0).skuPackages().stream().collect(java.util.stream.Collectors.toMap(DropshipRequests.SkuPackages::skuCode,DropshipRequests.SkuPackages::packageCount)));
|
||||
}
|
||||
@Test void consumesV2LoadingEventAndKeepsDropshipSnapshot(){
|
||||
EventEnvelope source=event();
|
||||
|
||||
@@ -1284,6 +1284,17 @@ CREATE TABLE IF NOT EXISTS ll_dropship_carton (
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_dropship_carton(customer_id,order_id,carton_mark), KEY idx_ll_dropship_carton_status(customer_id,status),
|
||||
KEY idx_ll_dropship_carton_sku(customer_id,sku_code,status)
|
||||
) ENGINE=InnoDB COMMENT='一件代发箱唛明细';
|
||||
CREATE TABLE IF NOT EXISTS ll_dropship_carton_sku (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', order_id BIGINT NOT NULL COMMENT '尾程订单ID',
|
||||
profile_id BIGINT NOT NULL COMMENT '一件代发托盘档案ID', carton_id BIGINT NOT NULL COMMENT '一件代发物理箱ID',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '箱内SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称快照',
|
||||
product_quantity BIGINT NOT NULL COMMENT '该物理箱内此SKU实收件数',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_dropship_carton_sku(carton_id,sku_code),
|
||||
KEY idx_ll_dropship_carton_sku_order(customer_id,order_id,sku_code),
|
||||
KEY idx_ll_dropship_carton_sku_profile(customer_id,profile_id,sku_code)
|
||||
) ENGINE=InnoDB COMMENT='一件代发物理箱内SKU明细';
|
||||
CREATE TABLE IF NOT EXISTS ll_dropship_sku_inventory (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', order_id BIGINT NOT NULL COMMENT '订单ID', profile_id BIGINT NOT NULL COMMENT '托盘档案ID',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称', package_count BIGINT NOT NULL COMMENT '箱数', product_quantity BIGINT NOT NULL COMMENT '产品数量',
|
||||
|
||||
Reference in New Issue
Block a user