From a5db6fbbb6c7b9c1a11f1945dd405db05ce34c08 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 15:14:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firstleg/AGENTS.md | 2 +- .../mapper/LoadingPickItemMapper.java | 24 +++----- .../mapper/LoadingPickTaskMapper.java | 5 +- .../firstleg/service/LoadingPickService.java | 49 +++++++-------- .../LoadingPickItemMapperContractTest.java | 6 +- .../service/LoadingPickServiceTest.java | 59 ++++++++++++++++--- 6 files changed, 86 insertions(+), 59 deletions(-) diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index d92bc8c77..98d136408 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -87,7 +87,7 @@ - 智能装柜全局工作台由服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选支持按创建日期和预计到货日期闭区间筛选,状态筛选使用最多四项的 `orderStatuses`,未传表示 `PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING` 全部未完成状态。状态只用于搜索和展示,能否加入以响应 `canAssign` 为准,由有效柜次占用、剩余箱数、容量、组锁和版本等事实决定;人工加入允许订单国内仓及渠道与柜次不同,智能推荐允许跨国内仓但仍只选择柜次渠道。容积和重量均允许达到柜型额定值的 150%,超过即拒绝。待审核事实必须经 `PendingReviewOrderClient` 重新核验 Order 权威 `SUBMITTED` 订单,待审核或待测量可先占位,但确认预装和启动 PDA 前仍必须进入 `WAIT_SPLIT`。计划明细通过 `totalPackageCount/remainingPackageCount` 返回可调总量和余量:测量前取 Order 原始申报箱数,进入 `WAIT_SPLIT/LOADING` 后取最新已审核测量箱数;只读补充失败时保守回退当前已分配数,调整写入仍重新查询并校验权威数量。同柜不可拆组只有全部成员命中状态集合时才返回,且必须成员完整、满箱并作为单个分页与分配单元,不能跨页或拆分。一次性移除未拣明细时,普通未拣项直接移除;同柜组仅全组未拣时整组移除,任一成员已拣则整组保留,单事务只递增一次计划版本。推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。 - 客户入库费用只在登录客户范围、测量审核通过、`feeStatus=SENT` 且存在已发布明细时可见;确认必须具备 `merchant:order:edit`,携带当前 `expectedFeeVersion` 与幂等键。成功后记录确认成员和时间;客户未确认或该票尚未装柜完成时,平台仍可修改箱规规则、生成新费用版本并重新发送,新版本必须清空旧版客户确认快照。仅当“客户已确认且该票已装柜完成”同时成立时最终冻结;装柜完成不得代替客户自动确认。旧版本、未发送或重复异参确认均返回冲突,不做兼容覆盖。 -- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。 +- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。装柜允许跨国内仓,仓库与渠道限制统一由上游装柜编排负责;PDA 只按已生成任务读取各入仓单自身的 `CONFIRMED` 库位资源,不得再用柜次仓库过滤任务明细、库存快照或拣货写入。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。 - PDA 部分拣货生成二级子入库单时,数量必须守恒:`已拣数量 + 子单数量 = 拣货前主单可用数量`。子单只允许一层,不能继续拆分。 - 首张称重图片确认时按 `max(已确认称重图片数, ceil(有效体积 / 默认托盘容量))` 尽力预留并确认整票库位块;有效体积先取入库单已落地的正数实测体积,否则对最新测量批次的正数 `totalVolumeCbm/originalVolumeCbm` 取较大值,再无有效值才查询 Order 最新客户申报体积,全部无效则拒绝。后续确认绑定最小空槽,所需槽数增长时通过 Warehouse `/blocks/expand` 原子追加,既有 `blockId/slotNo/reservationId/locationId/sharedOverflow` 不得变化。库位指引失败只记录警告,不阻断称重确认;测量提交后以最新已确认图片调用 `ensureForSubmission` 再次补齐和校验,失败同样不回滚已提交的测量主流程。Firstleg 必须显式保存 `weightResultId -> blockId/slotNo/reservationId/locationId/locationCode`,按 `confirmedAt + weightResultId` 稳定顺序绑定,禁止按列表下标推断;普通连续位不足的新增槽由 Warehouse 放入共享 A0,不阻断确认。 - 删除称重图片只解除图片和托盘快照绑定,将槽位恢复为 `RESERVED_EMPTY`;槽位和整票块继续归当前入仓码。解绑失败时不得调用文件删除,文件删除失败时本地解绑和重量结果删除必须回滚。 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java index 13ca4ad68..a7caf6af2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java @@ -139,15 +139,13 @@ public interface LoadingPickItemMapper extends BaseMapper JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id AND plan.id = pick_task.loading_plan_id - JOIN fl_container_booking booking ON booking.id = plan.container_booking_id JOIN fl_inbound_order inbound_order ON inbound_order.id = plan_item.inbound_id AND inbound_order.tenant_id = plan_item.account_id AND inbound_order.deleted = 0 - WHERE pick_item.pick_task_id = #{taskId} AND booking.warehouse_id = #{warehouseId} - AND plan_item.deleted = 0 + WHERE pick_item.pick_task_id = #{taskId} AND plan_item.deleted = 0 ORDER BY pick_item.id ASC """) java.util.List selectTaskItemsWithAccount(@Param("taskId") long taskId, - @Param("warehouseId") long warehouseId); + @Param("warehouseId") long warehouseId); @Select(""" SELECT pick_item.* @@ -156,11 +154,10 @@ public interface LoadingPickItemMapper extends BaseMapper JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id AND plan.id = pick_task.loading_plan_id - JOIN fl_container_booking booking ON booking.id = plan.container_booking_id JOIN fl_inbound_order inbound_order ON inbound_order.id = plan_item.inbound_id AND inbound_order.tenant_id = plan_item.account_id AND inbound_order.deleted = 0 WHERE pick_item.pick_task_id = #{taskId} AND pick_item.id = #{itemId} - AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0 + AND plan_item.deleted = 0 """) LoadingPickItemEntity selectTaskItemWithAccount(@Param("taskId") long taskId, @Param("itemId") long itemId, @@ -174,10 +171,8 @@ public interface LoadingPickItemMapper extends BaseMapper version = version + 1 WHERE id = #{id} AND version = #{version} AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item - JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id - JOIN fl_container_booking booking ON booking.id = plan.container_booking_id WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id - AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0) + AND plan_item.deleted = 0) AND picked_package_count + #{count} <= planned_package_count """) int pick(@Param("id") long id, @Param("count") int count, @@ -190,10 +185,8 @@ public interface LoadingPickItemMapper extends BaseMapper version = version + 1 WHERE id = #{id} AND version = #{version} AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item - JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id - JOIN fl_container_booking booking ON booking.id = plan.container_booking_id WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id - AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0) + AND plan_item.deleted = 0) AND item_status IN ('PENDING', 'PICKING') AND picked_package_count < #{finalCount} AND #{finalCount} < planned_package_count @@ -207,13 +200,12 @@ public interface LoadingPickItemMapper extends BaseMapper SET item_status = 'SKIPPED', version = version + 1 WHERE id = #{id} AND version = #{version} AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item - JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id - JOIN fl_container_booking booking ON booking.id = plan.container_booking_id WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id - AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0) + AND plan_item.deleted = 0) AND item_status = 'PENDING' AND picked_package_count = 0 """) - int skip(@Param("id") long id, @Param("version") int version, @Param("warehouseId") long warehouseId); + int skip(@Param("id") long id, @Param("version") int version, + @Param("warehouseId") long warehouseId); @Select(""" SELECT COUNT(*) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java index fcd8fed0e..614173ae9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickTaskMapper.java @@ -50,7 +50,6 @@ public interface LoadingPickTaskMapper extends BaseMapper JOIN fl_container_booking booking ON booking.id = plan.container_booking_id WHERE booking.container_no = #{containerNo} AND booking.record_status = 'ACTIVE' AND task.task_status = 'LOADING' - AND booking.warehouse_id = #{warehouseId} LIMIT 1 """) LoadingPickTaskEntity selectLoadingByContainerAndAccount(@Param("containerNo") String containerNo, @@ -59,13 +58,13 @@ public interface LoadingPickTaskMapper extends BaseMapper @Select("SELECT task.* 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 " + "WHERE task.task_status='LOADING' AND booking.record_status='ACTIVE' " - + "AND booking.cabinet_no=#{cabinetNo} AND booking.warehouse_id=#{warehouseId} LIMIT 1") + + "AND booking.cabinet_no=#{cabinetNo} LIMIT 1") LoadingPickTaskEntity selectLoadingByCabinet(@Param("cabinetNo") String cabinetNo, @Param("warehouseId") long warehouseId); @Select("SELECT task.* 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 " + "WHERE task.task_status='LOADING' AND booking.record_status='ACTIVE' " - + "AND task.loading_plan_id=#{loadingPlanId} AND booking.warehouse_id=#{warehouseId} LIMIT 1") + + "AND task.loading_plan_id=#{loadingPlanId} LIMIT 1") LoadingPickTaskEntity selectLoadingByPlan(@Param("loadingPlanId") long loadingPlanId, @Param("warehouseId") long warehouseId); @Select("SELECT booking.* FROM fl_loading_pick_task task " diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java index 8484f92cd..31c05dd43 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java @@ -242,7 +242,7 @@ public class LoadingPickService { public InventorySnapshot inventorySnapshot(long taskId, long warehouseId) { LoadingPickTaskEntity task = requiredTask(taskId); - List items = warehouseScopedItems(task, warehouseId); + List items = taskItems(task, warehouseId); return buildInventorySnapshot(task, warehouseId, items); } @@ -322,10 +322,7 @@ public class LoadingPickService { assertReplay(replay, taskId, itemId, requestHash); return view(task, request.warehouseId()); } - LoadingPickItemEntity item = repository.item(taskId, itemId, request.warehouseId()); - if (item == null) { - throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在"); - } + LoadingPickItemEntity item = requireTaskItem(taskId, itemId, request.warehouseId()); if (repository.pick(itemId, request.pickedPackageCount(), request.version(), request.warehouseId()) != 1) { throw conflict("拣货数量超过计划箱数或明细版本已变化"); } @@ -364,7 +361,7 @@ public class LoadingPickService { assertReplay(replay, taskId, itemId, requestHash); return view(task, request.warehouseId()); } - requireWarehouseItem(taskId, itemId, request.warehouseId()); + requireTaskItem(taskId, itemId, request.warehouseId()); if (repository.partial(itemId, request.finalPickedPackageCount(), request.version(), request.warehouseId()) != 1) { throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化"); @@ -396,7 +393,7 @@ public class LoadingPickService { assertReplay(replay, taskId, itemId, requestHash); return view(task, request.warehouseId()); } - LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId()); + LoadingPickItemEntity item = requireTaskItem(taskId, itemId, request.warehouseId()); if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) { throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选"); } @@ -897,10 +894,10 @@ public class LoadingPickService { return task; } - private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) { + private LoadingPickItemEntity requireTaskItem(long taskId, long itemId, long warehouseId) { LoadingPickItemEntity item = repository.item(taskId, itemId, warehouseId); if (item == null) { - throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在或不属于当前仓库"); + throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在或不属于当前任务"); } return item; } @@ -910,16 +907,12 @@ public class LoadingPickService { if (booking == null || !Objects.equals(task.getContainerBookingId(), booking.getId())) { throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或柜次已非有效存续记录"); } - if (warehouseId != null && !Objects.equals(warehouseId, booking.getWarehouseId())) { - throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或不属于当前仓库"); - } List source = warehouseId == null ? repository.items(task.getId()) : repository.items(task.getId(), warehouseId); Map> cartonsByItem = repository.cartonLabels(task.getId()).stream() .collect(Collectors.groupingBy(CartonLabelRow::getPickItemId)); Map tenantByInbound = tenantByInbound(task.getId()); - Map> inventoryByInbound = inventoryByInbound( - booking.getWarehouseId(), source, tenantByInbound); + Map> inventoryByInbound = inventoryByInbound(source, tenantByInbound); List items = source.stream().map(item -> { List cartons = cartonsByItem.getOrDefault(item.getId(), List.of()); CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0); @@ -967,11 +960,11 @@ public class LoadingPickService { private List validateBatchLines(LoadingPickTaskEntity task, BatchRequest request) { long taskId = task.getId(); List allItems = repository.items(taskId); - List scopedItems = repository.items(taskId, request.warehouseId()); - if (allItems.isEmpty() || scopedItems.size() != allItems.size()) { - throw conflict("PDA任务柜次、仓库或客户归属不一致"); + List taskItems = repository.items(taskId, request.warehouseId()); + if (allItems.isEmpty() || taskItems.size() != allItems.size()) { + throw conflict("PDA任务柜次或客户归属不一致"); } - Map itemsById = scopedItems.stream().collect(Collectors.toMap( + Map itemsById = taskItems.stream().collect(Collectors.toMap( LoadingPickItemEntity::getId, item -> item)); Set unresolvedIds = allItems.stream().filter(item -> !isResolved(item)) .map(LoadingPickItemEntity::getId).collect(Collectors.toSet()); @@ -989,7 +982,7 @@ public class LoadingPickService { LoadingPickItemEntity item = itemsById.get(requestItem.itemId()); if (item == null || item.getInboundId() == null || item.getLoadingPlanItemId() == null || item.getInboundDetailId() == null) { - throw conflict("PDA明细不属于当前任务、柜次、仓库或客户"); + throw conflict("PDA明细不属于当前任务、柜次或客户"); } if (!"SKIP".equals(requestItem.action())) { inboundHasWork.put(item.getInboundId(), true); @@ -1041,19 +1034,19 @@ public class LoadingPickService { return result; } - private List warehouseScopedItems(LoadingPickTaskEntity task, long warehouseId) { + private List taskItems(LoadingPickTaskEntity task, long warehouseId) { List allItems = repository.items(task.getId()); - List scopedItems = repository.items(task.getId(), warehouseId); - if (allItems.isEmpty() || scopedItems.size() != allItems.size()) { - throw conflict("PDA任务柜次、仓库或客户归属不一致"); + List taskItems = repository.items(task.getId(), warehouseId); + if (allItems.isEmpty() || taskItems.size() != allItems.size()) { + throw conflict("PDA任务柜次或客户归属不一致"); } - return scopedItems; + return taskItems; } private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId, List items) { Map> inventoryByInbound = inventoryByInbound( - warehouseId, items, tenantByInbound(task.getId())); + items, tenantByInbound(task.getId())); List inbounds = items.stream().map(LoadingPickItemEntity::getInboundId) .filter(Objects::nonNull).distinct().sorted().map(inboundId -> { List resources = inventoryByInbound.getOrDefault(inboundId, List.of()); @@ -1077,8 +1070,7 @@ public class LoadingPickService { })); } - private Map> inventoryByInbound(long warehouseId, - List items, + private Map> inventoryByInbound(List items, Map tenantByInbound) { Map> result = new HashMap<>(); for (Long inboundId : items.stream().map(LoadingPickItemEntity::getInboundId) @@ -1086,7 +1078,6 @@ public class LoadingPickService { Long tenantId = tenantByInbound.get(inboundId); if (tenantId == null) throw conflict("PDA任务缺少入库单客户归属"); List resources = palletAllocations.list(tenantId, inboundId).stream() - .filter(resource -> Objects.equals(resource.getWarehouseId(), warehouseId)) .filter(resource -> "CONFIRMED".equals(resource.getAllocationStatus())) .sorted(Comparator.comparing(InboundPalletAllocationEntity::getBlockId, Comparator.nullsLast(Long::compareTo)) @@ -1134,7 +1125,7 @@ public class LoadingPickService { private void validateInventorySnapshot(LoadingPickTaskEntity task, BatchRequest request, List lines) { - List allItems = warehouseScopedItems(task, request.warehouseId()); + List allItems = taskItems(task, request.warehouseId()); InventorySnapshot current = buildInventorySnapshot(task, request.warehouseId(), allItems); if (!current.snapshotToken().equals(request.inventorySnapshotToken().trim())) { throw conflict("库存已变化,请刷新后重试"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickItemMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickItemMapperContractTest.java index 39050982e..3050653cc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickItemMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingPickItemMapperContractTest.java @@ -89,16 +89,16 @@ class LoadingPickItemMapperContractTest { } @Test - void pdaWritesAndReadsRequireWarehouseScope() throws Exception { + void pdaWritesAndReadsRelyOnUpstreamTaskScopeInsteadOfWarehouseScope() throws Exception { Method list = LoadingPickItemMapper.class.getDeclaredMethod("selectTaskItemsWithAccount", long.class, long.class); Method pick = LoadingPickItemMapper.class.getDeclaredMethod("pick", long.class, int.class, int.class, long.class); assertThat(list.getAnnotation(Select.class).value()[0]) - .contains("booking.warehouse_id = #{warehouseId}") + .doesNotContain("booking.warehouse_id = #{warehouseId}") .contains("plan_item.deleted = 0"); assertThat(pick.getAnnotation(Update.class).value()[0]) - .contains("booking.warehouse_id = #{warehouseId}") + .doesNotContain("booking.warehouse_id = #{warehouseId}") .contains("plan_item.deleted = 0"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index dc043ee27..238cc534d 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -511,16 +511,22 @@ class LoadingPickServiceTest { } @Test - void taskDetailRejectsWarehouseThatDoesNotOwnTheContainer() { + void taskDetailDoesNotRecheckWarehouseOwnership() { LoadingPickTaskEntity task = task(); when(repository.task(20L)).thenReturn(task); + when(repository.items(20L)).thenReturn(List.of(item())); + when(repository.items(20L, 999L)).thenReturn(List.of(item())); + 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, 2, 202L))); - assertThatThrownBy(() -> service.task(20L, 999L)) - .isInstanceOf(BizException.class).hasMessageContaining("不属于当前仓库"); + var result = service.task(20L, 999L); - verify(repository, never()).items(20L, 999L); - verify(repository, never()).cartonLabels(20L); - verify(completionCommands, never()).latest(20L); + assertThat(result.id()).isEqualTo(20L); + assertThat(result.items()).hasSize(1); + assertThat(result.items().get(0).inventoryResources()).singleElement() + .extracting("availablePackageCount").isEqualTo(2); } @Test @@ -1308,6 +1314,40 @@ class LoadingPickServiceTest { verify(settlementService, never()).settle(20L, 9L, "operator"); } + @Test + void batchUsesConfirmedInboundInventoryWhenContainerLoadsAcrossWarehouses() { + LoadingPickTaskEntity task = task(); + LoadingPickItemEntity item = item(); + item.setPlannedPackageCount(250); + when(repository.taskForUpdate(20L)).thenReturn(task); + when(repository.task(20L)).thenReturn(task); + when(repository.assignment(20L)).thenReturn(assignment()); + when(repository.items(20L)).thenReturn(List.of(item)); + when(repository.items(20L, 101L)).thenReturn(List.of(item)); + when(repository.inboundOwners(20L)).thenReturn(List.of( + new LoadingPickItemMapper.InboundOwner(1L, 50L))); + when(palletAllocations.list(1L, 50L)).thenReturn(List.of(allocation(50L, 250, 202L))); + when(repository.pick(30L, 250, 0, 101L)).thenReturn(1); + when(repository.allTaskItemsResolved(20L)).thenReturn(true); + when(repository.touchTask(20L, 0)).thenReturn(1); + + var snapshot = service.inventorySnapshot(20L, 101L); + assertThat(snapshot.warehouseId()).isEqualTo(101L); + assertThat(snapshot.inbounds()).singleElement().satisfies(inbound -> { + assertThat(inbound.availablePackageCount()).isEqualTo(250); + assertThat(inbound.resources()).singleElement() + .extracting("availablePackageCount") + .isEqualTo(250); + }); + + service.batch(20L, "batch-cross-warehouse", new BatchRequest( + 101L, 10L, 99L, 0, snapshot.snapshotToken(), List.of( + new BatchItemRequest(30L, "PICK", 250, null, 0, null))), 9L, "operator"); + + verify(repository).pick(30L, 250, 0, 101L); + verify(repository).touchTask(20L, 0); + } + @Test void rejectsBatchTaskVersionConflictBeforeWriting() { LoadingPickTaskEntity task = task(); @@ -1457,6 +1497,11 @@ class LoadingPickServiceTest { } private static InboundPalletAllocationEntity allocation(long inboundId, int packageCount) { + return allocation(inboundId, packageCount, 101L); + } + + private static InboundPalletAllocationEntity allocation(long inboundId, int packageCount, + long warehouseId) { InboundPalletAllocationEntity allocation = new InboundPalletAllocationEntity(); allocation.setId(72L); allocation.setTenantId(1L); @@ -1464,7 +1509,7 @@ class LoadingPickServiceTest { allocation.setBlockId(71L); allocation.setSlotNo(1); allocation.setAllocationNo(1); - allocation.setWarehouseId(101L); + allocation.setWarehouseId(warehouseId); allocation.setPalletId(73L); allocation.setLocationId(74L); allocation.setLocationCode("A-01");