Merge origin/test into dev
This commit is contained in:
@@ -246,7 +246,9 @@ public interface InboundSplitSettlementMapper {
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan_item plan_item
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id = plan_item.id
|
||||
SET plan_item.package_count = pick_item.picked_package_count,
|
||||
SET plan_item.pallet_count = CEIL(COALESCE(plan_item.pallet_count,0)
|
||||
* pick_item.picked_package_count / NULLIF(plan_item.package_count,0)),
|
||||
plan_item.package_count = pick_item.picked_package_count,
|
||||
plan_item.updated_by = #{operatorId}, plan_item.updated_by_name = #{operatorName},
|
||||
plan_item.updated_at = CURRENT_TIMESTAMP(3), plan_item.version = plan_item.version + 1
|
||||
WHERE plan_item.id = #{planItemId} AND plan_item.deleted = 0
|
||||
|
||||
@@ -174,7 +174,13 @@ public interface LoadingListQueryMapper {
|
||||
SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no,
|
||||
i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no,
|
||||
i.sku_code, i.destination_warehouse_name, i.product_name_zh,
|
||||
product_name_en, product_image_url, package_count, pallet_count,
|
||||
product_name_en, product_image_url, i.package_count,
|
||||
COALESCE(NULLIF(i.pallet_count,0),
|
||||
CEIL(COALESCE(NULLIF(inbound.pallet_count,0),
|
||||
(SELECT COUNT(*) FROM fl_inbound_weight_result weight
|
||||
WHERE weight.tenant_id=i.account_id AND weight.inbound_id=i.inbound_id
|
||||
AND weight.deleted=0 AND weight.recognition_status='CONFIRMED'),0)
|
||||
* i.package_count / NULLIF(inbound.total_package_count,0)),0) pallet_count,
|
||||
gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg,
|
||||
product_quantity, units_per_carton, fba_tracking_no, shipment_no,
|
||||
shipping_mark, internal_remark, customer_remark, warehouse_remark,
|
||||
@@ -182,6 +188,7 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_loading_plan_item i
|
||||
JOIN fl_loading_plan p ON p.id=i.loading_plan_id
|
||||
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
|
||||
LEFT JOIN fl_inbound_order inbound ON inbound.id=i.inbound_id AND inbound.tenant_id=i.account_id
|
||||
WHERE i.loading_plan_id = #{planId} AND i.deleted = 0
|
||||
ORDER BY i.load_sequence, i.id
|
||||
""")
|
||||
|
||||
@@ -6,11 +6,13 @@ import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanItemMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanMapper;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -26,11 +28,17 @@ public class SmartLoadingRepository {
|
||||
@Resource private LoadingPlanHistoryMapper histories;
|
||||
@Resource private ContainerBookingMapper bookings;
|
||||
@Resource private SmartLoadingCandidateMapper candidates;
|
||||
@Resource private InboundOrderMapper inbounds;
|
||||
|
||||
public List<Candidate> candidates(long warehouseId) { return candidates.candidates(warehouseId); }
|
||||
public Candidate candidate(long warehouseId, long inboundId, long detailId) {
|
||||
return candidates.candidate(warehouseId, inboundId, detailId);
|
||||
}
|
||||
public InboundOrderEntity inbound(long accountId, long inboundId) {
|
||||
return inbounds.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, accountId)
|
||||
.eq(InboundOrderEntity::getId, inboundId));
|
||||
}
|
||||
public List<LoadingPlanItemEntity> activeItemsByInbound(long planId, long accountId, long inboundId) {
|
||||
return items.selectList(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
|
||||
|
||||
@@ -136,7 +136,8 @@ public class SmartLoadingService {
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
requireSameChannel(booking, candidate);
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
if ("LOADING".equals(plan.getPlanStatus())) {
|
||||
var task = loadingPickService.mutationSnapshot(planId);
|
||||
if (task.taskId() != null && "LOADING".equals(task.taskStatus())) {
|
||||
splitPreloadService.ensureForRunningLoadingAllocation(candidate.accountId(), candidate.inboundId(),
|
||||
operatorId, operatorName);
|
||||
} else {
|
||||
@@ -161,7 +162,8 @@ public class SmartLoadingService {
|
||||
} else {
|
||||
existing.setPackageCount(total); existing.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(total)));
|
||||
existing.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(total)));
|
||||
existing.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(total))); applyCargo(existing, cargo, total);
|
||||
existing.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(total)));
|
||||
existing.setPalletCount(estimatedPalletCount(candidate, total)); applyCargo(existing, cargo, total);
|
||||
existing.setUpdatedBy(operatorId); existing.setUpdatedByName(operatorName);
|
||||
if (repository.update(existing) != 1) throw conflict("装柜草稿已被并发修改");
|
||||
changed = existing;
|
||||
@@ -317,7 +319,8 @@ public class SmartLoadingService {
|
||||
item.setPackageCount(request.packageCount());
|
||||
item.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount()))); applyCargo(item, cargo, request.packageCount());
|
||||
item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setPalletCount(estimatedPalletCount(candidate, request.packageCount())); applyCargo(item, cargo, request.packageCount());
|
||||
item.setUpdatedBy(operatorId); item.setUpdatedByName(operatorName);
|
||||
if (repository.update(item) != 1) throw conflict("装柜明细已被并发修改");
|
||||
history(planId, itemId, "ADJUST_ITEM", plan.getPlanStatus(), plan.getPlanStatus(), operation,
|
||||
@@ -448,8 +451,16 @@ public class SmartLoadingService {
|
||||
i.setGrossWeightKg(unit(c.unitGrossWeightKg()).multiply(BigDecimal.valueOf(count)));
|
||||
i.setNetWeightKg(unit(c.unitNetWeightKg()).multiply(BigDecimal.valueOf(count)));
|
||||
i.setVolumeCbm(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count)));
|
||||
i.setPalletCount(estimatedPalletCount(c, count));
|
||||
applyCargo(i, cargo, count); i.setLoadSequence(seq); i.setDeleted(false); i.setCreatedBy(op); i.setCreatedByName(name); i.setUpdatedBy(op); i.setUpdatedByName(name); i.setVersion(0); return i;
|
||||
}
|
||||
private int estimatedPalletCount(Candidate candidate, int packageCount) {
|
||||
InboundOrderEntity inbound = repository.inbound(candidate.accountId(), candidate.inboundId());
|
||||
if (inbound == null || inbound.getPalletCount() == null || inbound.getPalletCount() <= 0
|
||||
|| inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() <= 0) return 0;
|
||||
return BigDecimal.valueOf(inbound.getPalletCount()).multiply(BigDecimal.valueOf(packageCount))
|
||||
.divide(BigDecimal.valueOf(inbound.getTotalPackageCount()), 0, RoundingMode.CEILING).intValueExact();
|
||||
}
|
||||
private void applyCargo(LoadingPlanItemEntity item, OrderCargoClient.CargoBinding cargo, int boxes) {
|
||||
item.setSkuCode(cargo.skuNo());
|
||||
item.setProductNameEn(cargo.cargoNameEn());
|
||||
|
||||
@@ -68,21 +68,22 @@ public class SplitPreloadService {
|
||||
@Transactional
|
||||
public void ensurePendingForLoadingAllocation(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName) {
|
||||
ensureForLoadingAllocation(tenantId, inboundId, operatorId, operatorName, false);
|
||||
ensureLoadingAllocationLifecycle(tenantId, inboundId, operatorId, operatorName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* A running loading task may re-add an inbound whose preload was confirmed by an earlier
|
||||
* incremental allocation. Draft plans must still use the stricter pending-only entry point.
|
||||
* A running PDA task may restore or add an unpicked allocation after the plan-level preload
|
||||
* confirmation. CONFIRMING remains immutable; CONFIRMED is accepted because the caller owns
|
||||
* the active PDA task lock and will synchronise the restored loading item immediately.
|
||||
*/
|
||||
@Transactional
|
||||
public void ensureForRunningLoadingAllocation(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName) {
|
||||
ensureForLoadingAllocation(tenantId, inboundId, operatorId, operatorName, true);
|
||||
ensureLoadingAllocationLifecycle(tenantId, inboundId, operatorId, operatorName, true);
|
||||
}
|
||||
|
||||
private void ensureForLoadingAllocation(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, boolean allowConfirmed) {
|
||||
private void ensureLoadingAllocationLifecycle(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, boolean allowConfirmed) {
|
||||
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
|
||||
requireWaitingSplit(inbound, "仅待分柜订单可以建立智能装柜预装计划");
|
||||
PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId);
|
||||
|
||||
+3
@@ -60,6 +60,9 @@ class LoadingListQueryMapperContractTest {
|
||||
assertThat(items)
|
||||
.contains("i.order_id", "i.order_no", "b.container_no")
|
||||
.contains("b.record_status='ACTIVE'")
|
||||
.contains("NULLIF(i.pallet_count,0)")
|
||||
.contains("fl_inbound_weight_result weight")
|
||||
.contains("weight.recognition_status='CONFIRMED'")
|
||||
.contains("i.loading_plan_id = #{planId}")
|
||||
.contains("i.deleted = 0");
|
||||
}
|
||||
|
||||
+11
@@ -124,6 +124,17 @@ class PdaSplitSettlementSqlContractTest {
|
||||
.contains("split_status='FULLY_SPLIT'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialPickingConvergesPalletSnapshotWithActualCartons() throws Exception {
|
||||
String converge = InboundSplitSettlementMapper.class.getMethod("convergePartialPlanItem",
|
||||
long.class, long.class, String.class)
|
||||
.getAnnotation(Update.class).value()[0];
|
||||
assertThat(converge)
|
||||
.contains("plan_item.pallet_count = CEIL")
|
||||
.contains("pick_item.picked_package_count / NULLIF(plan_item.package_count,0)")
|
||||
.contains("plan_item.package_count = pick_item.picked_package_count");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsAllocationConfirmedUntilReleaseStep() throws Exception {
|
||||
String consume = InboundPalletAllocationMapper.class.getMethod("consume",
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
@@ -300,6 +301,9 @@ class SmartLoadingServiceTest {
|
||||
@Test
|
||||
void snapshotsCargoFields() {
|
||||
stubAssignment();
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(10L); inbound.setTenantId(9L); inbound.setTotalPackageCount(10); inbound.setPalletCount(4);
|
||||
when(repository.inbound(9L, 10L)).thenReturn(inbound);
|
||||
when(cargoClient.detail(anyString(), eq(9L), eq(30L), eq(20L))).thenReturn(ApiResponse.success(
|
||||
new OrderCargoClient.CargoBinding(20L, 30L, 1, "中文品名", 10,
|
||||
new BigDecimal("20"), new BigDecimal("2"), "SKU-1", "English name",
|
||||
@@ -320,6 +324,7 @@ class SmartLoadingServiceTest {
|
||||
assertThat(saved.getProductNameEn()).isEqualTo("English name");
|
||||
assertThat(saved.getUnitsPerCarton()).isEqualTo(4);
|
||||
assertThat(saved.getProductQuantity()).isEqualTo(20);
|
||||
assertThat(saved.getPalletCount()).isEqualTo(2);
|
||||
assertThat(saved.getNetWeightKg()).isEqualByComparingTo("7.5");
|
||||
assertThat(saved.getCustomerRemark()).isEqualTo("客户备注");
|
||||
assertThat(saved.getInternalRemark()).isEqualTo("最新内部备注");
|
||||
@@ -354,6 +359,8 @@ class SmartLoadingServiceTest {
|
||||
when(repository.items(1)).thenReturn(List.of());
|
||||
when(repository.candidate(100, 10, 20)).thenReturn(candidate(10));
|
||||
when(repository.activeItem(1L, 9L, 20L)).thenReturn(null);
|
||||
when(loadingPickService.mutationSnapshot(1L)).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(77L, "LOADING", 6, Map.of()));
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn(ApiResponse.success(
|
||||
new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 1,
|
||||
BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, null)));
|
||||
@@ -361,14 +368,15 @@ class SmartLoadingServiceTest {
|
||||
Plan result = service.assign(1, new AssignRequest(10L, 20L, 1, 5, "assign-loading"), 7, "u");
|
||||
|
||||
assertThat(result.status()).isEqualTo("LOADING");
|
||||
verify(splitPreloadService).ensureForRunningLoadingAllocation(9L, 10L, 7L, "u");
|
||||
verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(anyLong(), anyLong(), anyLong(), anyString());
|
||||
var order = inOrder(loadingPickService, repository, loadingModelBridge);
|
||||
var order = inOrder(loadingPickService, splitPreloadService, repository, loadingModelBridge);
|
||||
order.verify(loadingPickService).lockMutable(1L);
|
||||
order.verify(splitPreloadService).ensureForRunningLoadingAllocation(9L, 10L, 7L, "u");
|
||||
order.verify(repository).touch(1, 5, 7, "u");
|
||||
order.verify(loadingModelBridge).bindIncrementalAllocation(1L, 9L, 10L,
|
||||
7L, "u", "assign-loading");
|
||||
order.verify(loadingPickService).sync(eq(1L), any(LoadingPlanItemEntity.class));
|
||||
verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(
|
||||
anyLong(), anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -92,4 +92,14 @@ class SplitPreloadServiceTest {
|
||||
verify(repository, never()).createPlan(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runningLoadingAllocationStillRejectsConfirmingPreload() {
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 3));
|
||||
PreloadPlanEntity plan = new PreloadPlanEntity();
|
||||
plan.setStatus("CONFIRMING");
|
||||
when(repository.findPlan(10, 20)).thenReturn(plan);
|
||||
|
||||
assertThatThrownBy(() -> service.ensureForRunningLoadingAllocation(10, 20, 7, "admin"))
|
||||
.hasMessageContaining("已进入确认流程");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user