diff --git a/.image/出库新增.png b/.image/出库新增.png new file mode 100644 index 00000000..cf9da66d Binary files /dev/null and b/.image/出库新增.png differ diff --git a/docs/superpowers/plans/2026-07-27-wms-move-inventory.md b/docs/superpowers/plans/2026-07-27-wms-move-inventory.md new file mode 100644 index 00000000..8f120ce1 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-wms-move-inventory.md @@ -0,0 +1,86 @@ +# WMS 移库管理功能实现计划 + +> **面向 AI 代理的工作者:** 在当前会话内按任务顺序执行,使用 TDD 验证核心找位和锁定行为。 + +**目标:** 实现移库单 CRUD、库存与目标仓位选择、仓位锁定,以及按来源载具自动找位建单的内部 API。 + +**架构:** WMS Server 新增独立 `moveinv` 业务包,主 Service 维护单据事务,`MoveTargetStructSelector` 专门负责目标仓位选择。数据库查询集中在 Mapper XML;跨模块自动建单契约放在 WMS API。前端沿用出库页面的 Vben 表格、抽屉和弹窗模式。 + +**技术栈:** Java 17、Spring Boot、MyBatis/MyBatis-Plus、OpenFeign、Vue 3、TypeScript、Ant Design Vue。 + +--- + +### 任务 1:数据模型和 Mapper + +**文件:** +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinv/MoveInvDO.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinvdtl/MoveInvDtlDO.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinv/MoveInvMapper.xml` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml` +- 修改:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java` +- 修改:`nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml` + +- [ ] 映射两张移库表全部字段。 +- [ ] 增加分页、详情、按载具查询来源库存、同层空闲目标仓位查询。 +- [ ] 增加移出锁、移入锁和按移库单释放锁的条件更新。 + +### 任务 2:目标仓位选择器 + +**文件:** +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelector.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelectorTest.java` + +- [ ] 先写测试,覆盖同层、距离排序和批次目标去重。 +- [ ] 运行测试确认因选择器不存在而失败。 +- [ ] 实现最小选择算法并运行测试通过。 + +### 任务 3:后端业务和管理端接口 + +**文件:** +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/*.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/MoveInvController.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java` +- 修改:`nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java` + +- [ ] 实现分页、详情、可用库存、可用目标仓位。 +- [ ] 实现新增、修改、删除事务。 +- [ ] 创建时给来源仓位加 `4` 移出锁、目标仓位加 `5` 移入锁。 +- [ ] 修改/删除时只释放当前移库单持有的锁。 + +### 任务 4:自动找位建单 API + +**文件:** +- 创建:`nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApi.java` +- 创建:`nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateReqDTO.java` +- 创建:`nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateRespDTO.java` +- 创建:`nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApiImpl.java` + +- [ ] API 只接收来源载具编码列表。 +- [ ] Service 逐载具选择同库区同层目标仓位,确保目标不重复。 +- [ ] 任意载具不可用或无目标时整单回滚。 + +### 任务 5:前端管理页面 + +**文件:** +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/moveinv/index.ts` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/data.ts` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/index.vue` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/form.vue` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/inventory-select.vue` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/struct-select.vue` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/detail.vue` + +- [ ] 实现列表和生成状态操作限制。 +- [ ] 新增/修改抽屉默认全屏并回填明细。 +- [ ] 每个载具分别选择不同目标仓位。 +- [ ] 实现详情展示。 + +### 任务 6:验证 + +- [ ] 运行后端核心测试。 +- [ ] 运行 WMS reactor 编译。 +- [ ] 运行前端类型检查。 +- [ ] 检查工作区差异,不覆盖既有用户修改。 diff --git a/docs/superpowers/specs/2026-07-27-wms-move-inventory-design.md b/docs/superpowers/specs/2026-07-27-wms-move-inventory-design.md new file mode 100644 index 00000000..9cea1156 --- /dev/null +++ b/docs/superpowers/specs/2026-07-27-wms-move-inventory-design.md @@ -0,0 +1,164 @@ +# WMS 移库管理功能设计 + +## 1. 目标 + +基于现有 `wms_moveinv`、`wms_moveinvdtl` 表实现完整的移库管理功能,包括移库单列表、新增、修改、删除、详情、可用库存选择、目标仓位选择,以及供“一键设置”直接调用的自动建单接口。 + +## 2. 功能范围 + +### 2.1 移库单列表 + +- 分页查询移库单主表,默认按主键倒序。 +- 展示单据编码、单据类型、业务日期、仓库、总数量、明细数、单据状态、生成方式、创建人、创建时间和备注。 +- 单据编码可进入详情页。 +- 生成状态的单据允许修改、删除;其他状态不允许修改、删除。 + +### 2.2 新增和修改 + +- 页面交互与现有出库单新增、修改保持一致。 +- 新增时先选择仓库及库区,再选择该库区内可用库存。 +- 库存按载具选择,每个载具生成一条 `wms_moveinvdtl`。 +- 每条明细必须单独选择一个目标仓位。 +- 同一张移库单内,不允许多条明细选择同一个目标仓位。 +- 修改页面回填主表和全部明细。 +- 修改保存时更新主表,释放旧明细关联的仓位锁,删除旧明细,插入新明细,并按新明细重新加锁。 +- 主表的 `total_qty`、`detail_count` 根据保存的明细汇总生成。 + +### 2.3 可用库存 + +库存来源为组盘数据和载具所在仓位,必须同时满足: + +- 组盘数据未删除,并处于入库可用状态。 +- 载具编码不为空。 +- 载具所在仓位已启用且未删除。 +- 来源仓位未被其他业务锁定。 +- 来源仓位属于当前选择的仓库和库区。 + +选择结果回填载具编码、来源仓位、来源库区、物料编码、批次、数量、计量单位及来源单号。 + +### 2.4 手工目标仓位选择 + +目标仓位必须满足: + +- 与来源仓位属于同一个仓库。 +- 与来源仓位属于同一个库区。 +- 与来源仓位具有相同的 `layer_num`。 +- 仓位已启用且未删除。 +- `lock_type = 0`,即仓位未锁定。 +- `storagevehicle_code` 为空,即仓位上没有载具。 +- 不能是来源仓位。 +- 不能是当前移库单其他明细已经选择的目标仓位。 + +## 3. 自动目标仓位选择器 + +新增独立的 `MoveTargetStructSelector`,集中封装自动找位规则。业务 Service 不直接编写找位排序逻辑,方便后续替换算法。 + +选择器输入: + +- 来源仓位。 +- 当前批次已经选择的目标仓位标识集合。 + +选择器输出: + +- 一个满足条件的目标仓位。 + +默认规则: + +1. 筛选同仓库、同库区、同层的空闲仓位。 +2. 排除来源仓位及当前批次已经选择的目标仓位。 +3. 优先选择与来源仓位 `col_num` 距离较近的仓位。 +4. 距离相同时按 `row_num`、`col_num`、`block_num`、`struct_id` 升序选择。 +5. 找不到目标仓位时抛出业务异常。 + +仓位候选查询 SQL 统一放在 `StrucAttrMapper.xml` 中,选择器只负责候选校验和顺序选择。 + +## 4. 一键设置自动建单接口 + +在 WMS API 模块开放自动创建移库单接口,供出库“一键设置”等跨模块业务调用。 + +请求参数: + +- 来源载具编码列表。 + +处理流程: + +1. 校验载具编码不可重复。 +2. 批量查询每个载具对应的来源仓位和可用库存。 +3. 对每个载具调用 `MoveTargetStructSelector`,自动选择同库区、同层目标仓位。 +4. 本次已选择的目标仓位加入排除集合,确保一车一目标且目标不重复。 +5. 生成一张移库单及对应明细。 +6. 任意载具不存在、库存不可用或找不到目标仓位时,整个事务回滚。 +7. 返回创建成功的移库单标识和单据编码。 + +接口生成方式使用接口创建对应的字典值,初始单据状态为“生成”。 + +## 5. 仓位锁定 + +锁定类型来自字典 `wms_lock_type`: + +- `4`:移出锁。 +- `5`:移入锁。 + +创建成功后: + +- 来源仓位设置 `lock_type = 4`。 +- 目标仓位设置 `lock_type = 5`。 +- 两类锁均写入 `inv_type`、`inv_id`、`inv_code`,关联当前移库单。 + +修改时: + +1. 校验单据处于生成状态。 +2. 仅释放 `inv_id` 等于当前移库单标识的旧来源、目标仓位锁。 +3. 更新主表并重建明细。 +4. 对新来源仓位加移出锁,对新目标仓位加移入锁。 + +删除时: + +- 仅允许删除生成状态单据。 +- 释放当前单据持有的来源和目标仓位锁。 +- 删除主表和明细。 + +建单、修改、删除、加锁和解锁必须处于同一数据库事务。 + +## 6. 数据一致性与并发 + +- 保存前再次在数据库中校验来源仓位和目标仓位状态,不能只依赖页面查询结果。 +- 仓位加锁更新必须带上当前未锁定条件,受影响行数不为 1 时视为仓位已被占用并回滚。 +- 同一载具不能在一张单据中重复出现。 +- 同一目标仓位不能在一张单据中重复出现。 +- 主表、明细和仓位锁任一步失败,整笔事务回滚。 + +## 7. 前端结构 + +- 新增移库管理 API 文件。 +- 新增移库列表页面。 +- 新增全屏移库新增/修改页面。 +- 新增库存选择弹窗。 +- 新增目标仓位选择弹窗。 +- 新增移库详情页面。 +- 表格列沿用系统全局分隔线、勾选框颜色和操作按钮图标规范。 + +## 8. 错误处理 + +需要提供明确的中文业务异常: + +- 移库单不存在。 +- 当前状态不允许修改或删除。 +- 来源载具不存在或库存不可用。 +- 来源仓位已锁定。 +- 目标仓位已占用或已锁定。 +- 目标仓位与来源仓位不在同一库区或同一层。 +- 目标仓位在当前单据中重复。 +- 未找到同库区、同层的可用目标仓位。 + +## 9. 验证范围 + +- 后端编译通过。 +- 前端类型检查通过。 +- 新增、修改时主表和明细正确保存。 +- 修改时旧锁释放、新锁正确创建。 +- 删除时来源和目标仓位锁正确释放。 +- 自动找位只选择同库区、同层、未锁定、无载具的仓位。 +- 批量自动建单目标仓位不重复。 +- 任意一条自动找位失败时不产生主表、明细或残留锁。 +- 起点锁定类型为 `4`,目的点锁定类型为 `5`。 diff --git a/nl-gateway/src/main/resources/application.yaml b/nl-gateway/src/main/resources/application.yaml index 0aaabc08..525e0055 100644 --- a/nl-gateway/src/main/resources/application.yaml +++ b/nl-gateway/src/main/resources/application.yaml @@ -216,6 +216,13 @@ spring: - Path=/admin-api/wms/** filters: - RewritePath=/admin-api/wms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs + ## task-server 服务 + - id: task-admin-api # 路由的编号 + uri: grayLb://task-server + predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 + - Path=/admin-api/task/** + filters: + - RewritePath=/admin-api/task/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs ## im-server 服务 - id: im-admin-api # 路由的编号 uri: grayLb://im-server @@ -252,6 +259,9 @@ knife4j: - name: base-server service-name: base-server url: /admin-api/base/v3/api-docs + - name: task-server + service-name: task-server + url: /admin-api/task/v3/api-docs - name: bpm-server service-name: bpm-server url: /admin-api/bpm/v3/api-docs diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java index c9579ac9..7c446a98 100644 --- a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/api/TransportTaskApi.java @@ -60,4 +60,9 @@ public interface TransportTaskApi { @Operation(summary = "根据 materialCode 和 ownerService 查询运行中任务") CommonResult> getRunningTaskByMaterialCode(@RequestParam("materialCode") String materialCode, @RequestParam("ownerService") String ownerService); + + @GetMapping(PREFIX + "/countUnfinishedByDestination") + @Operation(summary = "统计指定目的点的未完成任务数") + CommonResult countUnfinishedByDestination(@RequestParam("pointCode") String pointCode, + @RequestParam("ownerService") String ownerService); } diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java index 2334120f..d44bcc13 100644 --- a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/dto/TransportTaskCreateReqDTO.java @@ -21,6 +21,12 @@ public class TransportTaskCreateReqDTO { @NotEmpty(message = "业务归属服务不能为空") private String ownerService; + @Schema(description = "业务类型") + private String bizType; + + @Schema(description = "业务侧标识") + private String bizId; + @Schema(description = "业务回调处理器编码", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "业务回调处理器编码不能为空") private String handleCode; diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java index 707e80c1..178eaa32 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/api/TransportTaskApiImpl.java @@ -69,4 +69,9 @@ public class TransportTaskApiImpl implements TransportTaskApi { public CommonResult> getRunningTaskByMaterialCode(String materialCode, String ownerService) { return success(transportTaskService.getRunningTaskInfoByMaterialCode(materialCode, ownerService)); } + + @Override + public CommonResult countUnfinishedByDestination(String pointCode, String ownerService) { + return success(transportTaskService.countUnfinishedByDestination(pointCode, ownerService)); + } } diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java index 3028c4f1..b276a568 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/dal/mysql/transporttask/TransportTaskMapper.java @@ -109,6 +109,14 @@ public interface TransportTaskMapper extends BaseMapperX { .orderByDesc(TransportTaskDO::getTaskId)); } + default Long selectUnfinishedCountByDestination(String pointCode, String ownerService) { + return selectCount(new LambdaQueryWrapperX() + .eq(TransportTaskDO::getPointCode2, pointCode) + .eq(TransportTaskDO::getOwnerService, ownerService) + .lt(TransportTaskDO::getTaskStatus, + TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode())); + } + /** * 条件更新任务状态(CAS 抢占) */ diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/rpc/config/RpcConfiguration.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/rpc/config/RpcConfiguration.java index 64e9c9d5..364e791e 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/rpc/config/RpcConfiguration.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/framework/rpc/config/RpcConfiguration.java @@ -1,11 +1,16 @@ package cn.code.nl.module.task.framework.rpc.config; +import cn.code.nl.module.base.api.classstandard.ClassStandardApi; +import cn.code.nl.module.base.api.codegen.CodeGenApi; +import cn.code.nl.module.infra.api.config.ConfigApi; +import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; /** * Task 服务 RPC 配置 - * Feign Client 注册已移至框架层 {@link cn.code.nl.framework.execute.config.NlExecuteRpcAutoConfiguration} + * 框架通用 Feign Client 由框架层注册,Task 业务直接依赖的 Client 在此注册。 */ @Configuration(value = "taskRpcConfiguration", proxyBeanMethods = false) +@EnableFeignClients(clients = {ConfigApi.class, ClassStandardApi.class, CodeGenApi.class}) public class RpcConfiguration { } diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java index 27057427..ddb09fb7 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskService.java @@ -123,6 +123,8 @@ public interface TransportTaskService { */ List getRunningTaskInfoByMaterialCode(String materialCode, String ownerService); + Long countUnfinishedByDestination(String pointCode, String ownerService); + /** * 自动下发待下发任务到 ACS */ diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java index f752f284..e1db57cf 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskServiceImpl.java @@ -230,6 +230,11 @@ public class TransportTaskServiceImpl implements TransportTaskService { return TransportTaskConvert.INSTANCE.convertList(tasks); } + @Override + public Long countUnfinishedByDestination(String pointCode, String ownerService) { + return transportTaskMapper.selectUnfinishedCountByDestination(pointCode, ownerService); + } + /** * 查询待下发任务,并委托下发管理器按生产区域下发到 ACS */ diff --git a/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml b/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml index 06832135..8fc3ff85 100644 --- a/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml +++ b/nl-module-task/nl-module-task-server/src/main/resources/application-dev.yaml @@ -7,12 +7,12 @@ spring: username: nacos password: nacos discovery: # 【注册中心】配置项 - namespace: dev # 命名空间。这里使用 dev 开发环境 + namespace: 91c8ac41-7fb0-423b-947e-2caad4c87d41 # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【配置中心】配置项 - namespace: dev # 命名空间。这里使用 dev 开发环境 + namespace: 91c8ac41-7fb0-423b-947e-2caad4c87d41 # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP --- #################### 数据库相关配置 #################### @@ -57,12 +57,12 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: root slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:mysql://192.168.81.193:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/huachuang_lms_dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: root @@ -100,11 +100,11 @@ spring: xxl: job: admin: - addresses: http://192.168.10.2:8080/xxl-job-admin # 调度中心部署跟地址 - accessToken: 123456 # 执行器通讯TOKEN + addresses: http://192.168.81.193:8080/xxl-job-admin # 调度中心部署跟地址 + accessToken: default_token # 执行器通讯TOKEN executor: - ip: 192.168.10.2 - port: 9994 + ip: localhost + port: 8995 --- #################### 服务保障相关配置 #################### diff --git a/nl-module-task/nl-module-task-server/src/main/resources/application.yaml b/nl-module-task/nl-module-task-server/src/main/resources/application.yaml index d63db646..ef670d4d 100644 --- a/nl-module-task/nl-module-task-server/src/main/resources/application.yaml +++ b/nl-module-task/nl-module-task-server/src/main/resources/application.yaml @@ -36,7 +36,7 @@ spring: time-to-live: 1h # 设置过期时间为 1 小时 server: - port: 48084 + port: 48086 logging: file: diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApi.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApi.java new file mode 100644 index 00000000..b790e1ce --- /dev/null +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApi.java @@ -0,0 +1,19 @@ +package cn.code.nl.module.wms.api.moveinv; + +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO; +import cn.code.nl.module.wms.enums.ApiConstants; +import jakarta.validation.Valid; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = ApiConstants.NAME) +public interface MoveInvApi { + + String PREFIX = ApiConstants.PREFIX + "/move-inv"; + + @PostMapping(PREFIX + "/autoCreate") + CommonResult autoCreate(@Valid @RequestBody MoveInvAutoCreateReqDTO reqDTO); +} diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateReqDTO.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateReqDTO.java new file mode 100644 index 00000000..28bf65cd --- /dev/null +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateReqDTO.java @@ -0,0 +1,16 @@ +package cn.code.nl.module.wms.api.moveinv.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +/** + * 自动创建移库单请求。 + */ +@Data +public class MoveInvAutoCreateReqDTO { + + @NotEmpty(message = "来源载具不能为空") + private List vehicleCodes; +} diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateRespDTO.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateRespDTO.java new file mode 100644 index 00000000..c0fe5c98 --- /dev/null +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/api/moveinv/dto/MoveInvAutoCreateRespDTO.java @@ -0,0 +1,17 @@ +package cn.code.nl.module.wms.api.moveinv.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 自动创建移库单结果。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MoveInvAutoCreateRespDTO { + + private Long moveinvId; + private String billCode; +} diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ApiConstants.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ApiConstants.java new file mode 100644 index 00000000..2d07ceb3 --- /dev/null +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ApiConstants.java @@ -0,0 +1,15 @@ +package cn.code.nl.module.wms.enums; + +import cn.code.nl.framework.common.enums.RpcConstants; + +/** + * WMS API 常量。 + */ +public final class ApiConstants { + + public static final String NAME = "wms-server"; + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/wms"; + + private ApiConstants() { + } +} diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java index 57c3fa7c..ae31ec67 100644 --- a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java @@ -19,6 +19,18 @@ public interface ErrorCodeConstants { ErrorCode IOSTOR_INV_NO_MATCHING_INVENTORY = new ErrorCode(6_000_6, "没有符合当前出库明细的可用库存"); ErrorCode IOSTOR_INV_MANUAL_STATUS_INVALID = new ErrorCode(6_000_7, "只有生成或分配中的明细允许手工分配"); ErrorCode IOSTOR_INV_UPDATE_STATUS_INVALID = new ErrorCode(6_000_8, "只有生成状态的出库单允许修改"); + ErrorCode IOSTOR_INV_NO_PENDING_ALLOCATION = new ErrorCode(6_000_9, "当前明细没有生成状态的分配记录"); + ErrorCode IOSTOR_INV_NO_MOVE_LOCATION = new ErrorCode(6_001_0, "没有可用于移库的空闲仓位"); + + // ================ 移库单相关错误码 ================= + ErrorCode MOVE_INV_NOT_EXISTS = new ErrorCode(6_002_0, "移库单不存在"); + ErrorCode MOVE_INV_STATUS_INVALID = new ErrorCode(6_002_1, "只有生成状态的移库单允许修改或删除"); + ErrorCode MOVE_INV_SOURCE_INVALID = new ErrorCode(6_002_2, "来源载具不存在、库存不可用或仓位已锁定"); + ErrorCode MOVE_INV_TARGET_INVALID = new ErrorCode(6_002_3, "目标仓位不可用或与来源仓位不在同一库区同一层"); + ErrorCode MOVE_INV_TARGET_DUPLICATE = new ErrorCode(6_002_4, "同一移库单的目标仓位不能重复"); + ErrorCode MOVE_INV_NO_TARGET_STRUCT = new ErrorCode(6_002_5, "未找到同库区同层的可用目标仓位"); + ErrorCode MOVE_INV_LOCK_FAILED = new ErrorCode(6_002_6, "移库仓位已被占用,请刷新后重试"); + ErrorCode MOVE_INV_CODE_GENERATE_FAILED = new ErrorCode(6_002_7, "移库单号生成失败"); ErrorCode IOSTORINV_DTL_NOT_EXISTS = new ErrorCode(5, "出入库单明细不存在"); diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/OutboundPointEnum.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/OutboundPointEnum.java new file mode 100644 index 00000000..7f9ad1ab --- /dev/null +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/OutboundPointEnum.java @@ -0,0 +1,21 @@ +package cn.code.nl.module.wms.enums; + +import lombok.Getter; + +/** + * 出库目的点。 + * + * TODO 配置现场实际的两个出库点位编码。 + */ +@Getter +public enum OutboundPointEnum { + + OUTBOUND_POINT_1("CK01"), + OUTBOUND_POINT_2("CK02"); + + private final String pointCode; + + OutboundPointEnum(String pointCode) { + this.pointCode = pointCode; + } +} diff --git a/nl-module-wms/nl-module-wms-server/pom.xml b/nl-module-wms/nl-module-wms-server/pom.xml index 26dc0cdf..dd6e9af2 100644 --- a/nl-module-wms/nl-module-wms-server/pom.xml +++ b/nl-module-wms/nl-module-wms-server/pom.xml @@ -141,6 +141,12 @@ h2 test + + cn.nl.cloud + nl-module-infra-api + 2026.06-SNAPSHOT + compile + diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApiImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApiImpl.java new file mode 100644 index 00000000..c4b3adb8 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/api/moveinv/MoveInvApiImpl.java @@ -0,0 +1,30 @@ +package cn.code.nl.module.wms.api.moveinv; + +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO; +import cn.code.nl.module.wms.service.moveinv.MoveInvService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static cn.code.nl.framework.common.pojo.CommonResult.success; + +/** + * 移库内部 API 实现。 + */ +@RestController +@Validated +public class MoveInvApiImpl implements MoveInvApi { + + @Resource + private MoveInvService moveInvService; + + /** + * 按来源载具自动创建移库单。 + */ + @Override + public CommonResult autoCreate(MoveInvAutoCreateReqDTO reqDTO) { + return success(moveInvService.autoCreate(reqDTO)); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java index e7c4a8e5..9e8a8a33 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java @@ -178,6 +178,14 @@ public class IostorInvController { return success(true); } + @PostMapping("/one-click-set") + @Operation(summary = "出库明细一键设置并下发任务") + @PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')") + public CommonResult oneClickSet(@Valid @RequestBody OutboundOneClickSetReqVO reqVO) { + iostorInvService.oneClickSet(reqVO); + return success(true); + } + @GetMapping("/page") @Operation(summary = "获得出入库单主表分页") @TransMethodResult diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/OutboundOneClickSetReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/OutboundOneClickSetReqVO.java new file mode 100644 index 00000000..d6c5ad3e --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/OutboundOneClickSetReqVO.java @@ -0,0 +1,16 @@ +package cn.code.nl.module.wms.controller.admin.iostorinv.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 出库明细一键设置 Request VO") +@Data +public class OutboundOneClickSetReqVO { + + @NotNull(message = "出库单标识不能为空") + private Long iostorinvId; + + @NotNull(message = "出库单明细标识不能为空") + private Long iostorinvdtlId; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/MoveInvController.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/MoveInvController.java new file mode 100644 index 00000000..8c2ba01f --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/MoveInvController.java @@ -0,0 +1,98 @@ +package cn.code.nl.module.wms.controller.admin.moveinv; + +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvPageReqVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvRespVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvSaveReqVO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import cn.code.nl.module.wms.service.moveinv.MoveInvService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import org.dromara.core.trans.anno.TransMethodResult; + +import static cn.code.nl.framework.common.pojo.CommonResult.success; + +/** + * 移库管理接口。 + */ +@Tag(name = "管理后台 - 移库管理") +@RestController +@RequestMapping("/wms/move-inv") +@Validated +public class MoveInvController { + + @Resource + private MoveInvService moveInvService; + + @PostMapping("/create") + @Operation(summary = "创建移库单") + @PreAuthorize("@ss.hasPermission('wms:movement-order:create')") + public CommonResult create(@Valid @RequestBody MoveInvSaveReqVO reqVO) { + return success(moveInvService.create(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改移库单") + @PreAuthorize("@ss.hasPermission('wms:movement-order:update')") + public CommonResult update(@Valid @RequestBody MoveInvSaveReqVO reqVO) { + moveInvService.update(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除移库单") + @PreAuthorize("@ss.hasPermission('wms:movement-order:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + moveInvService.delete(id); + return success(true); + } + + @GetMapping("/get") + @TransMethodResult + @Operation(summary = "查询移库单详情") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult get(@RequestParam("id") Long id) { + return success(moveInvService.get(id)); + } + + @GetMapping("/page") + @TransMethodResult + @Operation(summary = "分页查询移库单") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult> getPage(@Valid MoveInvPageReqVO reqVO) { + return success(moveInvService.getPage(reqVO)); + } + + @GetMapping("/availableInventory") + @Operation(summary = "查询移库可用库存") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult> getAvailableInventory( + @RequestParam("storId") String storId, + @RequestParam(value = "sectId", required = false) String sectId) { + return success(moveInvService.getAvailableInventory(storId, sectId)); + } + + @GetMapping("/availableTargetStructs") + @Operation(summary = "查询同库区同层可用目标仓位") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult> getAvailableTargetStructs( + @RequestParam("sourceStructCode") String sourceStructCode) { + return success(moveInvService.getAvailableTargetStructs(sourceStructCode)); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvDetailSaveReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvDetailSaveReqVO.java new file mode 100644 index 00000000..533223c7 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvDetailSaveReqVO.java @@ -0,0 +1,29 @@ +package cn.code.nl.module.wms.controller.admin.moveinv.vo; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 移库明细保存参数。 + */ +@Data +public class MoveInvDetailSaveReqVO { + + @NotBlank(message = "来源载具不能为空") + private String storagevehicleCode; + @NotBlank(message = "来源仓位不能为空") + private String turnoutStructCode; + @NotBlank(message = "目标仓位不能为空") + private String turninStructCode; + private String turnoutSectCode; + private String turninSectCode; + private String materialCode; + private String pcsn; + private String qtyUnitId; + private String qtyUnitName; + private BigDecimal qty; + private String sourceBillCode; + private String remark; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvInventoryRespVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvInventoryRespVO.java new file mode 100644 index 00000000..427011a4 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvInventoryRespVO.java @@ -0,0 +1,28 @@ +package cn.code.nl.module.wms.controller.admin.moveinv.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 移库可用库存。 + */ +@Data +public class MoveInvInventoryRespVO { + + private String structId; + private String structCode; + private String structName; + private String sectId; + private String sectCode; + private String sectName; + private String storId; + private String storagevehicleCode; + private Integer layerNum; + private String materialCode; + private String pcsn; + private String qtyUnitId; + private String qtyUnitName; + private BigDecimal qty; + private String sourceBillCode; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java new file mode 100644 index 00000000..8e2a1a84 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java @@ -0,0 +1,16 @@ +package cn.code.nl.module.wms.controller.admin.moveinv.vo; + +import cn.code.nl.framework.common.pojo.PageParam; +import lombok.Data; + +/** + * 移库单分页参数。 + */ +@Data +public class MoveInvPageReqVO extends PageParam { + + private String billCode; + private String billType; + private String storId; + private String billStatus; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvRespVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvRespVO.java new file mode 100644 index 00000000..7fbab9c3 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvRespVO.java @@ -0,0 +1,37 @@ +package cn.code.nl.module.wms.controller.admin.moveinv.vo; + +import cn.code.nl.module.wms.dal.dataobject.moveinvdtl.MoveInvDtlDO; +import lombok.Data; +import cn.code.nl.module.system.api.user.AdminUserApi; +import org.dromara.core.trans.anno.Trans; +import org.dromara.core.trans.constant.TransType; +import org.dromara.core.trans.vo.VO; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 移库单响应。 + */ +@Data +public class MoveInvRespVO implements VO { + + private Long moveinvId; + private String billCode; + private String billType; + private String bizDate; + private String storId; + private String storName; + private BigDecimal totalQty; + private Integer detailCount; + private String billStatus; + private String remark; + private String createMode; + @Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX, + fields = "nickname", ref = "creatorName") + private String creator; + private String creatorName; + private LocalDateTime createTime; + private List details; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvSaveReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvSaveReqVO.java new file mode 100644 index 00000000..5e603fa4 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvSaveReqVO.java @@ -0,0 +1,27 @@ +package cn.code.nl.module.wms.controller.admin.moveinv.vo; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +/** + * 移库单保存参数。 + */ +@Data +public class MoveInvSaveReqVO { + + private Long moveinvId; + @NotBlank(message = "单据类型不能为空") + private String billType; + @NotBlank(message = "业务日期不能为空") + private String bizDate; + @NotBlank(message = "仓库不能为空") + private String storId; + private String remark; + @Valid + @NotEmpty(message = "移库明细不能为空") + private List details; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinv/MoveInvDO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinv/MoveInvDO.java new file mode 100644 index 00000000..c6ee767f --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinv/MoveInvDO.java @@ -0,0 +1,33 @@ +package cn.code.nl.module.wms.dal.dataobject.moveinv; + +import cn.code.nl.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 移库单主表。 + */ +@TableName("wms_moveinv") +@Data +@EqualsAndHashCode(callSuper = true) +public class MoveInvDO extends BaseDO { + + @TableId + private Long moveinvId; + private String billCode; + private String billType; + private String bizDate; + private String storId; + private BigDecimal totalQty; + private Integer detailCount; + private String billStatus; + private String remark; + private String createMode; + private String confirmor; + private LocalDateTime confirmTime; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinvdtl/MoveInvDtlDO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinvdtl/MoveInvDtlDO.java new file mode 100644 index 00000000..d3e6c62e --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/moveinvdtl/MoveInvDtlDO.java @@ -0,0 +1,35 @@ +package cn.code.nl.module.wms.dal.dataobject.moveinvdtl; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 移库单明细。 + */ +@TableName("wms_moveinvdtl") +@Data +public class MoveInvDtlDO { + + @TableId + private Long moveinvdtlId; + private Long moveinvId; + private Integer seqNo; + private String turnoutSectCode; + private String turnoutStructCode; + private String materialCode; + private String pcsn; + private String qtyUnitId; + private String qtyUnitName; + private BigDecimal qty; + private String turninSectCode; + private String turninStructCode; + private String workStatus; + private Long taskId; + private String storagevehicleCode; + private String isIssued; + private String sourceBillCode; + private String remark; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java new file mode 100644 index 00000000..c86e05cc --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java @@ -0,0 +1,24 @@ +package cn.code.nl.module.wms.dal.mysql.moveinv; + +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvPageReqVO; +import cn.code.nl.module.wms.dal.dataobject.moveinv.MoveInvDO; +import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +/** + * 移库单主表 Mapper。 + */ +@Mapper +public interface MoveInvMapper extends BaseMapperX { + + default PageResult selectPage(MoveInvPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MoveInvDO::getBillCode, reqVO.getBillCode()) + .eqIfPresent(MoveInvDO::getBillType, reqVO.getBillType()) + .eqIfPresent(MoveInvDO::getStorId, reqVO.getStorId()) + .eqIfPresent(MoveInvDO::getBillStatus, reqVO.getBillStatus()) + .orderByDesc(MoveInvDO::getMoveinvId)); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java new file mode 100644 index 00000000..33691d73 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java @@ -0,0 +1,19 @@ +package cn.code.nl.module.wms.dal.mysql.moveinvdtl; + +import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; +import cn.code.nl.module.wms.dal.dataobject.moveinvdtl.MoveInvDtlDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 移库单明细 Mapper。 + */ +@Mapper +public interface MoveInvDtlMapper extends BaseMapperX { + + List selectByMoveinvId(@Param("moveinvId") Long moveinvId); + + int deleteByMoveinvId(@Param("moveinvId") Long moveinvId); +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java index c9309808..9c47471d 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java @@ -6,6 +6,7 @@ import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; @@ -19,6 +20,28 @@ import cn.code.nl.module.wms.controller.admin.structAttr.vo.*; @Mapper public interface StrucAttrMapper extends BaseMapperX { + List selectMoveAvailableInventory( + @Param("storId") String storId, + @Param("sectId") String sectId, + @Param("vehicleCodes") Collection vehicleCodes); + + List selectMoveTargetCandidates( + @Param("storId") String storId, + @Param("sectId") String sectId, + @Param("layerNum") Integer layerNum); + + StrucAttrDO selectByStructCode(@Param("structCode") String structCode); + + int lockForMoveOut(@Param("structCode") String structCode, + @Param("invId") String invId, + @Param("invCode") String invCode); + + int lockForMoveIn(@Param("structCode") String structCode, + @Param("invId") String invId, + @Param("invCode") String invCode); + + int unlockMoveInv(@Param("invId") String invId); + @Update(""" UPDATE wms_structattr SET lock_type = '3', @@ -49,6 +72,17 @@ public interface StrucAttrMapper extends BaseMapperX { int unlockOutbound(@Param("vehicleCode") String vehicleCode, @Param("invId") String invId); + @Update(""" + UPDATE wms_structattr + SET lock_type = '2', + task_code = #{taskCode} + WHERE struct_id = #{structId} + AND deleted = '0' + AND (lock_type IS NULL OR lock_type IN ('0', '1', '3')) + """) + int lockForTask(@Param("structId") String structId, + @Param("taskCode") String taskCode); + default PageResult selectPage(StrucAttrPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(StrucAttrDO::getStructCode, reqVO.getStructCode()) diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java index 8ef36bd0..672d1fa1 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java @@ -1,6 +1,8 @@ package cn.code.nl.module.wms.framework.rpc.config; import cn.code.nl.module.base.api.codegen.CodeGenApi; +import cn.code.nl.module.infra.api.config.ConfigApi; +import cn.code.nl.module.task.api.TransportTaskApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @@ -10,6 +12,6 @@ import org.springframework.context.annotation.Configuration; * @Date: 2026/7/22 13:50 */ @Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients(clients = {CodeGenApi.class}) +@EnableFeignClients(clients = {CodeGenApi.class, TransportTaskApi.class, ConfigApi.class}) public class RpcConfiguration { } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java index 9ee0b052..084dcc7c 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java @@ -100,6 +100,8 @@ public interface IostorInvService { Long iostorinvId, Long iostorinvdtlId, String sectId); void manualAllocate(@Valid ManualAllocationSaveReqVO reqVO); + void oneClickSet(@Valid OutboundOneClickSetReqVO reqVO); + /** * 获得出入库单主表分页 * diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java index 3cab7820..bde3b7f7 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java @@ -6,6 +6,8 @@ import cn.hutool.core.util.StrUtil; import cn.code.nl.framework.common.pojo.CommonResult; import cn.code.nl.module.base.api.codegen.CodeGenApi; import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; +import cn.code.nl.module.task.api.TransportTaskApi; +import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; import cn.code.nl.module.wms.dal.dataobject.bsrealstorattr.BsrealStorAttrDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; import lombok.extern.slf4j.Slf4j; @@ -30,6 +32,7 @@ import cn.code.nl.module.wms.dal.mysql.iostorinvdis.IostorinvDisMapper; import cn.code.nl.module.wms.dal.mysql.structAttr.StrucAttrMapper; import cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -38,6 +41,8 @@ import static cn.code.nl.framework.common.util.collection.CollectionUtils.conver import static cn.code.nl.framework.common.util.collection.CollectionUtils.convertSet; import static cn.code.nl.framework.common.util.collection.CollectionUtils.diffList; import static cn.code.nl.module.wms.enums.ErrorCodeConstants.*; +import static cn.code.nl.module.wms.enums.OutboundPointEnum.OUTBOUND_POINT_1; +import static cn.code.nl.module.wms.enums.OutboundPointEnum.OUTBOUND_POINT_2; /** * 出入库单主表 Service 实现类 @@ -70,6 +75,9 @@ public class IostorInvServiceImpl implements IostorInvService { @Resource private StrucAttrMapper strucAttrMapper; + @Resource + private TransportTaskApi transportTaskApi; + /** * 创建出库单及其明细。 */ @@ -300,10 +308,12 @@ public class IostorInvServiceImpl implements IostorInvService { @Override @Transactional(rollbackFor = Exception.class) public void allocateAll(OutboundAllocationReqVO reqVO) { + // 1. 校验出库单主表,后续所有明细分配必须归属于同一张有效单据。 IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); if (header == null) { throw exception(IOSTOR_INV_NOT_EXISTS); } + // 2. 按明细序号遍历整张出库单;库区未选择时不限制库区,选择后只匹配指定库区库存。 List details = iostorinvDtlMapper.selectList( new LambdaQueryWrapperX() .eq(IostorinvDtlDO::getIostorinvId, reqVO.getIostorinvId()) @@ -311,6 +321,7 @@ public class IostorInvServiceImpl implements IostorInvService { for (IostorinvDtlDO detail : details) { allocateDetail(header, detail, reqVO.getSectId()); } + // 3. 全部明细处理完成后,根据各明细已分配数统一回算主表状态。 refreshHeaderAllocationStatus(reqVO.getIostorinvId()); } @@ -331,11 +342,13 @@ public class IostorInvServiceImpl implements IostorInvService { @Override @Transactional(rollbackFor = Exception.class) public void cancelAllAllocations(OutboundAllocationReqVO reqVO) { + // 1. 只查询“生成”状态的分配记录;已下发、执行中或完成的记录禁止通过全部取消删除。 List generated = iostorinvDisMapper.selectList( new LambdaQueryWrapperX() .eq(IostorinvDisDO::getIostorinvId, reqVO.getIostorinvId()) .eqIfPresent(IostorinvDisDO::getSectId, reqVO.getSectId()) .eq(IostorinvDisDO::getWorkStatus, "10")); + // 2. 逐条释放冻结库存和出库锁,删除分配记录,最后回算明细及主表状态。 cancelGeneratedAllocations(reqVO.getIostorinvId(), generated); } @@ -399,6 +412,183 @@ public class IostorInvServiceImpl implements IostorInvService { refreshHeaderAllocationStatus(reqVO.getIostorinvId()); } + @Override + public void oneClickSet(OutboundOneClickSetReqVO reqVO) { + // 1. 校验出库单和当前选中明细,避免为其他单据的明细生成任务。 + IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); + if (header == null) { + throw exception(IOSTOR_INV_NOT_EXISTS); + } + IostorinvDtlDO detail = getOwnedDetail(reqVO.getIostorinvId(), reqVO.getIostorinvdtlId()); + // 2. 只处理尚未创建任务的“生成”状态分配记录,保证一键设置不会重复建任务。 + List pending = iostorinvDisMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(reqVO.getIostorinvId())) + .eq(IostorinvDisDO::getIostorinvdtlId, String.valueOf(reqVO.getIostorinvdtlId())) + .eq(IostorinvDisDO::getWorkStatus, "10") + .isNull(IostorinvDisDO::getTaskId) + .orderByAsc(IostorinvDisDO::getSeqNo)); + if (CollUtil.isEmpty(pending)) { + throw exception(IOSTOR_INV_NO_PENDING_ALLOCATION); + } + + // 3. 同一载具可能对应多个子卷分配,先按载具归组,后续一台载具只创建一个出库任务。 + Map> allocationsByVehicle = pending.stream() + .collect(java.util.stream.Collectors.groupingBy(IostorinvDisDO::getStoragevehicleCode, + LinkedHashMap::new, java.util.stream.Collectors.toList())); + // 4. 收集本次真正需要出库的仓位,用于识别同排货物中的目标箱和挡路箱。 + Set targetStructIds = pending.stream() + .map(IostorinvDisDO::getStructId) + .filter(Objects::nonNull) + .collect(java.util.stream.Collectors.toSet()); + List targetStructs = strucAttrMapper.selectBatchIds(targetStructIds); + Map targetStructMap = convertMap(targetStructs, + StrucAttrDO::getStructId, item -> item); + + // 5. 按“库区 + 块 + 排 + 层”分组,同一物理通道内的货物一起规划左右出库顺序。 + Map> targetGroups = targetStructs.stream() + .collect(java.util.stream.Collectors.groupingBy(this::placementGroupKey, + LinkedHashMap::new, java.util.stream.Collectors.toList())); + // 6. 预先准备移库空位、两个出库点当前负载,以及本批任务共用的任务组和执行序号。 + List freeMoveLocations = selectFreeMoveLocations(header.getStorId(), targetStructIds); + Iterator freeLocationIterator = freeMoveLocations.iterator(); + long[] destinationLoads = loadDestinationCounts(); + long taskGroupId = IdUtil.getSnowflakeNextId(); + long sortSeq = 0; + + for (List targets : targetGroups.values()) { + StrucAttrDO sample = targets.get(0); + // 7. 查询当前排、当前层的全部仓位,按列号还原真实摆放顺序。 + List rowSlots = strucAttrMapper.selectList( + new LambdaQueryWrapperX() + .eq(StrucAttrDO::getSectId, sample.getSectId()) + .eq(StrucAttrDO::getBlockNum, sample.getBlockNum()) + .eq(StrucAttrDO::getRowNum, sample.getRowNum()) + .eq(StrucAttrDO::getLayerNum, sample.getLayerNum()) + .eq(StrucAttrDO::getIsUsed, true) + .orderByAsc(StrucAttrDO::getColNum)); + List slots = rowSlots.stream() + .map(item -> new OutboundPlacementPlanner.Slot(item.getStructId(), + Objects.requireNonNullElse(item.getColNum(), 0), + item.getStoragevehicleCode(), targetStructIds.contains(item.getStructId()))) + .toList(); + // 8. 根据 placement_type 计算左出、右出或双向最少移库方案,结果已按执行顺序排列。 + List actions = + OutboundPlacementPlanner.plan(sample.getPlacementType(), slots); + for (OutboundPlacementPlanner.Action action : actions) { + // 目标箱可直接从映射获取;挡路箱不在分配记录中,需要从当前排仓位补充来源信息。 + StrucAttrDO source = targetStructMap.get(action.structId()); + if (source == null) { + source = rowSlots.stream() + .filter(item -> item.getStructId().equals(action.structId())) + .findFirst().orElseThrow(); + } + if ("MOVE".equals(action.type())) { + // 9. 挡路箱创建移库任务,并锁定起点、目的仓位,防止规划后被其他任务占用。 + if (!freeLocationIterator.hasNext()) { + throw exception(IOSTOR_INV_NO_MOVE_LOCATION); + } + StrucAttrDO destination = freeLocationIterator.next(); + Long taskId = createAndIssueTask("010505", "移库任务", source.getStructCode(), + destination.getStructCode(), action.vehicleCode(), null, null, + taskGroupId, ++sortSeq, "MOVE", action.structId()); + strucAttrMapper.lockForTask(source.getStructId(), String.valueOf(taskId)); + strucAttrMapper.lockForTask(destination.getStructId(), String.valueOf(taskId)); + continue; + } + // 10. 非本次分配的载具只参与通道规划,不创建出库任务。 + List vehicleAllocations = allocationsByVehicle.get(action.vehicleCode()); + if (CollUtil.isEmpty(vehicleAllocations)) { + continue; + } + // 11. 比较两个出库点未完成任务数,选择当前负载较小的目的点。 + String destinationPoint = chooseDestination(destinationLoads); + Long taskId = createAndIssueTask("010503", "出库任务", source.getStructCode(), + destinationPoint, action.vehicleCode(), detail.getMaterialId(), + detail.getMaterialCode(), taskGroupId, ++sortSeq, "OUT", + vehicleAllocations.get(0).getIostorinvdisId()); + destinationLoads[destinationPoint.equals(OUTBOUND_POINT_1.getPointCode()) ? 0 : 1]++; + // 12. 同载具的全部分配记录关联同一个任务,并推进到“执行中/已下发”状态。 + vehicleAllocations.forEach(allocation -> { + IostorinvDisDO update = new IostorinvDisDO(); + update.setIostorinvdisId(allocation.getIostorinvdisId()); + update.setTaskId(String.valueOf(taskId)); + update.setPointCode(destinationPoint); + update.setWorkStatus("20"); + update.setIsIssued("1"); + iostorinvDisMapper.updateById(update); + }); + // 13. 锁定出库起点仓位,直到任务回调完成后再由后续流程释放。 + strucAttrMapper.lockForTask(source.getStructId(), String.valueOf(taskId)); + } + } + } + + private List selectFreeMoveLocations(String storId, Set excludedStructIds) { + return strucAttrMapper.selectList(new LambdaQueryWrapperX() + .eq(StrucAttrDO::getStorId, storId) + .eq(StrucAttrDO::getIsUsed, true) + .and(wrapper -> wrapper.isNull(StrucAttrDO::getStoragevehicleCode) + .or().eq(StrucAttrDO::getStoragevehicleCode, "")) + .and(wrapper -> wrapper.isNull(StrucAttrDO::getLockType) + .or().in(StrucAttrDO::getLockType, "0", "1")) + .notIn(CollUtil.isNotEmpty(excludedStructIds), StrucAttrDO::getStructId, excludedStructIds) + .orderByAsc(StrucAttrDO::getSectId) + .orderByAsc(StrucAttrDO::getBlockNum) + .orderByAsc(StrucAttrDO::getRowNum) + .orderByAsc(StrucAttrDO::getColNum)); + } + + private String placementGroupKey(StrucAttrDO item) { + return String.join("|", String.valueOf(item.getSectId()), String.valueOf(item.getBlockNum()), + String.valueOf(item.getRowNum()), String.valueOf(item.getLayerNum())); + } + + private long[] loadDestinationCounts() { + Long point1 = transportTaskApi.countUnfinishedByDestination( + OUTBOUND_POINT_1.getPointCode(), "WMS").getCheckedData(); + Long point2 = transportTaskApi.countUnfinishedByDestination( + OUTBOUND_POINT_2.getPointCode(), "WMS").getCheckedData(); + return new long[]{Objects.requireNonNullElse(point1, 0L), Objects.requireNonNullElse(point2, 0L)}; + } + + private String chooseDestination(long[] destinationLoads) { + return destinationLoads[0] <= destinationLoads[1] + ? OUTBOUND_POINT_1.getPointCode() : OUTBOUND_POINT_2.getPointCode(); + } + + private Long createAndIssueTask(String taskType, String taskName, String sourcePoint, + String destinationPoint, String vehicleCode, String materialId, + String materialCode, long taskGroupId, long sortSeq, + String bizType, String bizId) { + TransportTaskCreateReqDTO task = new TransportTaskCreateReqDTO(); + task.setTaskName(taskName); + task.setOwnerService("WMS"); + task.setBizType(bizType); + task.setBizId(bizId); + // TODO 填写出库任务在 WMS 中对应的任务回调处理器编码。 + task.setHandleCode("OUTTASK"); + task.setTaskType(taskType); + task.setAcsTaskType("7"); + // TODO 填写现场使用的 AGV 系统类型。 + task.setAgvSystemType("1"); + task.setPointCode1(sourcePoint); + task.setPointCode2(destinationPoint); + task.setVehicleCode(vehicleCode); + if (StrUtil.isNotBlank(materialId)) { + task.setMaterialId(Long.valueOf(materialId)); + } + task.setMaterialCode(materialCode); + task.setTaskGroupId(taskGroupId); + task.setSortSeq(sortSeq); + task.setIsAutoIssue("0"); + task.setProductArea("LK"); + task.setIsCreateFinish(true); + Long taskId = transportTaskApi.createTransportTask(task).getCheckedData(); + transportTaskApi.issueTransportTaskRollBack(taskId).getCheckedData(); + return taskId; + } + private void validateManualAllocationStatus(IostorinvDtlDO detail) { if (!Set.of("10", "20").contains(detail.getBillStatus())) { throw exception(IOSTOR_INV_MANUAL_STATUS_INVALID); @@ -446,6 +636,7 @@ public class IostorInvServiceImpl implements IostorInvService { } private BigDecimal allocateDetail(IostorInvDO header, IostorinvDtlDO detail, String sectId) { + // 从明细当前已分配数量继续补分,避免重复分配已经满足的计划数量。 BigDecimal assigned = Objects.requireNonNullElse(detail.getAssignQty(), BigDecimal.ZERO); BigDecimal remaining = detail.getPlanQty().subtract(assigned).max(BigDecimal.ZERO); BigDecimal allocatedNow = BigDecimal.ZERO; @@ -453,6 +644,7 @@ public class IostorInvServiceImpl implements IostorInvService { updateDetailAllocation(detail, assigned); return allocatedNow; } + // 按物料、批次、来源单号匹配入库可用库存;sectId 有值时额外限制库区。 List candidates = iostorinvDtlMapper.selectAllocationInventoryForUpdate( header.getStorId(), sectId, detail.getMaterialId(), detail.getPcsn(), @@ -461,7 +653,9 @@ public class IostorInvServiceImpl implements IostorInvService { if (remaining.signum() == 0) { break; } + // 单次分配量不超过明细剩余计划量,也不超过当前库存可用量。 BigDecimal allocationQty = remaining.min(candidate.getAvailableQty()); + // 写入生成状态分配记录,等待后续一键设置创建并下发搬运任务。 IostorinvDisDO allocation = new IostorinvDisDO(); allocation.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr()); allocation.setIostorinvId(String.valueOf(header.getIostorinvId())); @@ -487,6 +681,7 @@ public class IostorInvServiceImpl implements IostorInvService { allocation.setHandType(false); iostorinvDisMapper.insert(allocation); + // 分配落库后立即加出库锁;并发情况下加锁失败则整个分配事务回滚。 int locked = strucAttrMapper.lockForOutbound( candidate.getStructId(), header.getBillType(), String.valueOf(header.getIostorinvId()), header.getBillCode()); @@ -494,6 +689,7 @@ public class IostorInvServiceImpl implements IostorInvService { throw exception(IOSTOR_INV_INVENTORY_INVALID); } + // 冻结本次分配的组盘数量,防止相同库存被其他出库单重复分配。 GroupPlateDO currentGroup = groupPlateMapper.selectById(candidate.getGroupId()); GroupPlateDO groupUpdate = new GroupPlateDO(); groupUpdate.setGroupId(candidate.getGroupId()); @@ -505,6 +701,7 @@ public class IostorInvServiceImpl implements IostorInvService { allocatedNow = allocatedNow.add(allocationQty); remaining = remaining.subtract(allocationQty); } + // 根据最终分配量更新明细的已分配数、未分配数及生成/分配中/分配完状态。 updateDetailAllocation(detail, assigned); return allocatedNow; } @@ -513,6 +710,7 @@ public class IostorInvServiceImpl implements IostorInvService { Set detailIds = new HashSet<>(); for (IostorinvDisDO allocation : generated) { detailIds.add(Long.valueOf(allocation.getIostorinvdtlId())); + // 1. 按载具、物料、批次定位原组盘库存,并退回该分配记录占用的冻结数量。 List groups = groupPlateMapper.selectList( new LambdaQueryWrapperX() .eq(GroupPlateDO::getVehicleCode, allocation.getStoragevehicleCode()) @@ -527,7 +725,9 @@ public class IostorInvServiceImpl implements IostorInvService { .subtract(allocation.getPlanQty()).max(BigDecimal.ZERO)); groupPlateMapper.updateById(update); }); + // 2. 仅删除调用方预先筛选出的“生成”状态分配记录。 iostorinvDisMapper.deleteById(allocation.getIostorinvdisId()); + // 3. 同一载具仍有其他分配时保留仓位锁;最后一条分配删除后才释放出库锁。 boolean vehicleStillAllocated = iostorinvDisMapper.selectCount( new LambdaQueryWrapperX() .eq(IostorinvDisDO::getStoragevehicleCode, @@ -537,6 +737,7 @@ public class IostorInvServiceImpl implements IostorInvService { allocation.getStoragevehicleCode(), String.valueOf(iostorinvId)); } } + // 4. 按取消后的剩余分配记录重新汇总每条受影响明细的分配数量和状态。 for (Long detailId : detailIds) { IostorinvDtlDO detail = iostorinvDtlMapper.selectById(detailId); BigDecimal assigned = iostorinvDisMapper.selectList( @@ -546,6 +747,7 @@ public class IostorInvServiceImpl implements IostorInvService { .reduce(BigDecimal.ZERO, BigDecimal::add); updateDetailAllocation(detail, assigned); } + // 5. 所有明细回算完成后,再统一更新主表的生成/分配中/分配完状态。 refreshHeaderAllocationStatus(iostorinvId); } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/OutboundPlacementPlanner.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/OutboundPlacementPlanner.java new file mode 100644 index 00000000..978208ea --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/OutboundPlacementPlanner.java @@ -0,0 +1,64 @@ +package cn.code.nl.module.wms.service.iostorinv; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 根据仓位放置类型规划一排货物的出库、移库顺序。 + */ +final class OutboundPlacementPlanner { + + private static final String DOUBLE_ACCESS = "01"; + private static final String LEFT_ACCESS = "02"; + private static final String RIGHT_ACCESS = "03"; + + private OutboundPlacementPlanner() { + } + + static List plan(String placementType, List slots) { + List occupied = slots.stream() + .filter(slot -> slot.vehicleCode() != null && !slot.vehicleCode().isBlank()) + .sorted(Comparator.comparingInt(Slot::column)) + .toList(); + if (occupied.stream().noneMatch(Slot::target)) { + return List.of(); + } + if (DOUBLE_ACCESS.equals(placementType)) { + List fromLeft = planFromSide(occupied, false); + List fromRight = planFromSide(occupied, true); + return moveCount(fromLeft) <= moveCount(fromRight) ? fromLeft : fromRight; + } + return planFromSide(occupied, RIGHT_ACCESS.equals(placementType)); + } + + private static List planFromSide(List occupied, boolean reverse) { + List ordered = new ArrayList<>(occupied); + if (reverse) { + ordered.sort(Comparator.comparingInt(Slot::column).reversed()); + } + int lastTargetIndex = -1; + for (int index = 0; index < ordered.size(); index++) { + if (ordered.get(index).target()) { + lastTargetIndex = index; + } + } + List result = new ArrayList<>(); + for (int index = 0; index <= lastTargetIndex; index++) { + Slot slot = ordered.get(index); + result.add(new Action(slot.target() ? "OUT" : "MOVE", + slot.structId(), slot.vehicleCode())); + } + return result; + } + + private static long moveCount(List actions) { + return actions.stream().filter(action -> "MOVE".equals(action.type())).count(); + } + + record Slot(String structId, int column, String vehicleCode, boolean target) { + } + + record Action(String type, String structId, String vehicleCode) { + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java new file mode 100644 index 00000000..5e9f5131 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java @@ -0,0 +1,34 @@ +package cn.code.nl.module.wms.service.moveinv; + +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvPageReqVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvRespVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvSaveReqVO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; + +import java.util.List; + +/** + * 移库管理 Service。 + */ +public interface MoveInvService { + + Long create(MoveInvSaveReqVO reqVO); + + void update(MoveInvSaveReqVO reqVO); + + void delete(Long id); + + MoveInvRespVO get(Long id); + + PageResult getPage(MoveInvPageReqVO reqVO); + + List getAvailableInventory(String storId, String sectId); + + List getAvailableTargetStructs(String sourceStructCode); + + MoveInvAutoCreateRespDTO autoCreate(MoveInvAutoCreateReqDTO reqDTO); +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java new file mode 100644 index 00000000..95535792 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java @@ -0,0 +1,370 @@ +package cn.code.nl.module.wms.service.moveinv; + +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.module.base.api.codegen.CodeGenApi; +import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateReqDTO; +import cn.code.nl.module.wms.api.moveinv.dto.MoveInvAutoCreateRespDTO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvDetailSaveReqVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvPageReqVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvRespVO; +import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvSaveReqVO; +import cn.code.nl.module.wms.dal.dataobject.moveinv.MoveInvDO; +import cn.code.nl.module.wms.dal.dataobject.moveinvdtl.MoveInvDtlDO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import cn.code.nl.module.wms.dal.mysql.moveinv.MoveInvMapper; +import cn.code.nl.module.wms.dal.mysql.moveinvdtl.MoveInvDtlMapper; +import cn.code.nl.module.wms.dal.mysql.structAttr.StrucAttrMapper; +import cn.code.nl.module.wms.dal.mysql.bsrealstorattr.BsrealStorAttrMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_CODE_GENERATE_FAILED; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_LOCK_FAILED; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_NOT_EXISTS; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_SOURCE_INVALID; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_STATUS_INVALID; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_TARGET_DUPLICATE; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_TARGET_INVALID; + +/** + * 移库管理 Service 实现。 + */ +@Service +public class MoveInvServiceImpl implements MoveInvService { + + private static final String GENERATED_STATUS = "10"; + private static final String MANUAL_CREATE_MODE = "01"; + private static final String API_CREATE_MODE = "02"; + + @Resource + private MoveInvMapper moveInvMapper; + @Resource + private MoveInvDtlMapper moveInvDtlMapper; + @Resource + private StrucAttrMapper strucAttrMapper; + @Resource + private CodeGenApi codeGenApi; + @Resource + private BsrealStorAttrMapper bsrealStorAttrMapper; + + /** + * 创建移库单。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(MoveInvSaveReqVO reqVO) { + return createMoveInv(reqVO, MANUAL_CREATE_MODE).getMoveinvId(); + } + + /** + * 更新移库单并替换全部明细。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(MoveInvSaveReqVO reqVO) { + MoveInvDO existed = moveInvMapper.selectById(reqVO.getMoveinvId()); + if (existed == null) { + throw exception(MOVE_INV_NOT_EXISTS); + } + if (!GENERATED_STATUS.equals(existed.getBillStatus())) { + throw exception(MOVE_INV_STATUS_INVALID); + } + strucAttrMapper.unlockMoveInv(String.valueOf(existed.getMoveinvId())); + List inventory = validateDetails(reqVO); + validateTargets(reqVO, inventory); + + MoveInvDO update = new MoveInvDO(); + update.setMoveinvId(existed.getMoveinvId()); + update.setBillType(reqVO.getBillType()); + update.setBizDate(reqVO.getBizDate()); + update.setStorId(reqVO.getStorId()); + update.setRemark(reqVO.getRemark()); + update.setDetailCount(reqVO.getDetails().size()); + update.setTotalQty(sumQty(reqVO.getDetails())); + moveInvMapper.updateById(update); + moveInvDtlMapper.deleteByMoveinvId(existed.getMoveinvId()); + insertDetailsAndLock(existed, reqVO.getDetails()); + } + + /** + * 删除生成状态移库单并释放仓位锁。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + MoveInvDO existed = moveInvMapper.selectById(id); + if (existed == null) { + throw exception(MOVE_INV_NOT_EXISTS); + } + if (!GENERATED_STATUS.equals(existed.getBillStatus())) { + throw exception(MOVE_INV_STATUS_INVALID); + } + strucAttrMapper.unlockMoveInv(String.valueOf(id)); + moveInvDtlMapper.deleteByMoveinvId(id); + moveInvMapper.deleteById(id); + } + + /** + * 查询移库单详情。 + */ + @Override + public MoveInvRespVO get(Long id) { + MoveInvDO moveInv = moveInvMapper.selectById(id); + if (moveInv == null) { + throw exception(MOVE_INV_NOT_EXISTS); + } + MoveInvRespVO respVO = convert(moveInv); + respVO.setDetails(moveInvDtlMapper.selectByMoveinvId(id)); + return respVO; + } + + /** + * 分页查询移库单。 + */ + @Override + public PageResult getPage(MoveInvPageReqVO reqVO) { + PageResult page = moveInvMapper.selectPage(reqVO); + List list = page.getList().stream().map(this::convert).toList(); + return new PageResult<>(list, page.getTotal()); + } + + /** + * 查询移库可用库存。 + */ + @Override + public List getAvailableInventory(String storId, String sectId) { + return strucAttrMapper.selectMoveAvailableInventory(storId, sectId, null); + } + + /** + * 查询来源仓位同库区同层的空闲目标仓位。 + */ + @Override + public List getAvailableTargetStructs(String sourceStructCode) { + StrucAttrDO source = strucAttrMapper.selectByStructCode(sourceStructCode); + if (source == null) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + return strucAttrMapper.selectMoveTargetCandidates( + source.getStorId(), source.getSectId(), source.getLayerNum()); + } + + /** + * 按来源载具自动寻找目标仓位并创建移库单。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public MoveInvAutoCreateRespDTO autoCreate(MoveInvAutoCreateReqDTO reqDTO) { + Set vehicleCodes = new HashSet<>(reqDTO.getVehicleCodes()); + if (vehicleCodes.size() != reqDTO.getVehicleCodes().size()) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + List inventory = strucAttrMapper.selectMoveAvailableInventory( + null, null, vehicleCodes); + if (inventory.size() != vehicleCodes.size()) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + String storId = inventory.get(0).getStorId(); + if (inventory.stream().anyMatch(item -> !storId.equals(item.getStorId()))) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + + Set selectedTargetIds = new HashSet<>(); + List details = new ArrayList<>(); + for (MoveInvInventoryRespVO item : inventory) { + StrucAttrDO source = strucAttrMapper.selectByStructCode(item.getStructCode()); + List candidates = strucAttrMapper.selectMoveTargetCandidates( + source.getStorId(), source.getSectId(), source.getLayerNum()); + StrucAttrDO target = MoveTargetStructSelector.select(source, candidates, selectedTargetIds); + selectedTargetIds.add(target.getStructId()); + details.add(buildDetail(item, target)); + } + + MoveInvSaveReqVO reqVO = new MoveInvSaveReqVO(); + reqVO.setBillType("3"); + reqVO.setBizDate(LocalDate.now().toString()); + reqVO.setStorId(storId); + reqVO.setDetails(details); + MoveInvDO moveInv = createMoveInv(reqVO, API_CREATE_MODE); + return new MoveInvAutoCreateRespDTO(moveInv.getMoveinvId(), moveInv.getBillCode()); + } + + /** + * 创建主表、明细并锁定来源和目标仓位。 + */ + private MoveInvDO createMoveInv(MoveInvSaveReqVO reqVO, String createMode) { + List inventory = validateDetails(reqVO); + validateTargets(reqVO, inventory); + CodeGenerateReqDTO codeReq = new CodeGenerateReqDTO(); + codeReq.setRuleCode("YK_CODE"); + CommonResult codeResult = codeGenApi.generate(codeReq); + if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) { + throw exception(MOVE_INV_CODE_GENERATE_FAILED); + } + + MoveInvDO moveInv = new MoveInvDO(); + moveInv.setBillCode(codeResult.getData()); + moveInv.setBillType(reqVO.getBillType()); + moveInv.setBizDate(reqVO.getBizDate()); + moveInv.setStorId(reqVO.getStorId()); + moveInv.setTotalQty(sumQty(reqVO.getDetails())); + moveInv.setDetailCount(reqVO.getDetails().size()); + moveInv.setBillStatus(GENERATED_STATUS); + moveInv.setRemark(reqVO.getRemark()); + moveInv.setCreateMode(createMode); + moveInvMapper.insert(moveInv); + insertDetailsAndLock(moveInv, reqVO.getDetails()); + return moveInv; + } + + /** + * 校验来源载具库存仍可用于移库。 + */ + private List validateDetails(MoveInvSaveReqVO reqVO) { + Set vehicleCodes = reqVO.getDetails().stream() + .map(MoveInvDetailSaveReqVO::getStoragevehicleCode) + .collect(Collectors.toSet()); + if (vehicleCodes.size() != reqVO.getDetails().size()) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + List inventory = strucAttrMapper.selectMoveAvailableInventory( + reqVO.getStorId(), null, vehicleCodes); + if (inventory.size() != vehicleCodes.size()) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + return inventory; + } + + /** + * 校验手工选择的目标仓位与来源仓位同库区同层且不重复。 + */ + private void validateTargets(MoveInvSaveReqVO reqVO, List inventory) { + Map inventoryMap = inventory.stream() + .collect(Collectors.toMap(MoveInvInventoryRespVO::getStoragevehicleCode, item -> item)); + Set targetCodes = new HashSet<>(); + for (MoveInvDetailSaveReqVO detail : reqVO.getDetails()) { + if (!targetCodes.add(detail.getTurninStructCode())) { + throw exception(MOVE_INV_TARGET_DUPLICATE); + } + MoveInvInventoryRespVO source = inventoryMap.get(detail.getStoragevehicleCode()); + StrucAttrDO target = strucAttrMapper.selectByStructCode(detail.getTurninStructCode()); + if (target == null || !"1".equals(target.getLockType()) + || StringUtils.hasText(target.getStoragevehicleCode()) + || !source.getStorId().equals(target.getStorId()) + || !source.getSectId().equals(target.getSectId()) + || !source.getLayerNum().equals(target.getLayerNum())) { + throw exception(MOVE_INV_TARGET_INVALID); + } + detail.setTurnoutSectCode(source.getSectCode()); + detail.setTurnoutStructCode(source.getStructCode()); + detail.setTurninSectCode(target.getSectCode()); + detail.setMaterialCode(source.getMaterialCode()); + detail.setPcsn(source.getPcsn()); + detail.setQtyUnitId(source.getQtyUnitId()); + detail.setQtyUnitName(source.getQtyUnitName()); + detail.setQty(source.getQty()); + detail.setSourceBillCode(source.getSourceBillCode()); + } + } + + /** + * 插入全部明细,并分别给来源和目标仓位加锁。 + */ + private void insertDetailsAndLock(MoveInvDO moveInv, List details) { + int seq = 1; + for (MoveInvDetailSaveReqVO detail : details) { + MoveInvDtlDO row = new MoveInvDtlDO(); + row.setMoveinvId(moveInv.getMoveinvId()); + row.setSeqNo(seq++); + row.setTurnoutSectCode(detail.getTurnoutSectCode()); + row.setTurnoutStructCode(detail.getTurnoutStructCode()); + row.setMaterialCode(detail.getMaterialCode()); + row.setPcsn(detail.getPcsn()); + row.setQtyUnitId(detail.getQtyUnitId()); + row.setQtyUnitName(detail.getQtyUnitName()); + row.setQty(detail.getQty()); + row.setTurninSectCode(detail.getTurninSectCode()); + row.setTurninStructCode(detail.getTurninStructCode()); + row.setWorkStatus("01"); + row.setStoragevehicleCode(detail.getStoragevehicleCode()); + row.setIsIssued("0"); + row.setSourceBillCode(detail.getSourceBillCode()); + row.setRemark(detail.getRemark()); + moveInvDtlMapper.insert(row); + + String invId = String.valueOf(moveInv.getMoveinvId()); + if (strucAttrMapper.lockForMoveOut(detail.getTurnoutStructCode(), invId, moveInv.getBillCode()) != 1 + || strucAttrMapper.lockForMoveIn(detail.getTurninStructCode(), invId, moveInv.getBillCode()) != 1) { + throw exception(MOVE_INV_LOCK_FAILED); + } + } + } + + /** + * 汇总移库数量。 + */ + private BigDecimal sumQty(List details) { + return details.stream().map(MoveInvDetailSaveReqVO::getQty) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * 构造自动找位明细。 + */ + private MoveInvDetailSaveReqVO buildDetail(MoveInvInventoryRespVO source, StrucAttrDO target) { + MoveInvDetailSaveReqVO detail = new MoveInvDetailSaveReqVO(); + detail.setStoragevehicleCode(source.getStoragevehicleCode()); + detail.setTurnoutSectCode(source.getSectCode()); + detail.setTurnoutStructCode(source.getStructCode()); + detail.setTurninSectCode(target.getSectCode()); + detail.setTurninStructCode(target.getStructCode()); + detail.setMaterialCode(source.getMaterialCode()); + detail.setPcsn(source.getPcsn()); + detail.setQtyUnitId(source.getQtyUnitId()); + detail.setQtyUnitName(source.getQtyUnitName()); + detail.setQty(source.getQty()); + detail.setSourceBillCode(source.getSourceBillCode()); + return detail; + } + + /** + * 转换移库单响应。 + */ + private MoveInvRespVO convert(MoveInvDO source) { + MoveInvRespVO target = new MoveInvRespVO(); + target.setMoveinvId(source.getMoveinvId()); + target.setBillCode(source.getBillCode()); + target.setBillType(source.getBillType()); + target.setBizDate(source.getBizDate()); + target.setStorId(source.getStorId()); + if (source.getStorId() != null) { + var stor = bsrealStorAttrMapper.selectById(source.getStorId()); + if (stor != null) { + target.setStorName(stor.getStorName()); + } + } + target.setTotalQty(source.getTotalQty()); + target.setDetailCount(source.getDetailCount()); + target.setBillStatus(source.getBillStatus()); + target.setRemark(source.getRemark()); + target.setCreateMode(source.getCreateMode()); + target.setCreator(source.getCreator()); + target.setCreateTime(source.getCreateTime()); + return target; + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelector.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelector.java new file mode 100644 index 00000000..04f287c9 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelector.java @@ -0,0 +1,40 @@ +package cn.code.nl.module.wms.service.moveinv; + +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.MOVE_INV_NO_TARGET_STRUCT; + +/** + * 移库目标仓位选择器。 + */ +public final class MoveTargetStructSelector { + + private MoveTargetStructSelector() { + } + + /** + * 从候选仓位中选择距离来源仓位最近且未被本批次使用的仓位。 + * + * @param source 来源仓位 + * @param candidates 同库区同层候选仓位 + * @param excludedStructIds 本批次已使用仓位 + * @return 目标仓位 + */ + public static StrucAttrDO select(StrucAttrDO source, List candidates, + Set excludedStructIds) { + return candidates.stream() + .filter(item -> !excludedStructIds.contains(item.getStructId())) + .min(Comparator + .comparingInt((StrucAttrDO item) -> Math.abs(item.getColNum() - source.getColNum())) + .thenComparing(StrucAttrDO::getRowNum) + .thenComparing(StrucAttrDO::getColNum) + .thenComparing(StrucAttrDO::getBlockNum) + .thenComparing(StrucAttrDO::getStructId)) + .orElseThrow(() -> exception(MOVE_INV_NO_TARGET_STRUCT)); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml new file mode 100644 index 00000000..b2738995 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + DELETE FROM wms_moveinvdtl WHERE moveinv_id = #{moveinvId} + + diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml index cc1b886b..2713b26f 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml @@ -1,12 +1,107 @@ - + - + - \ No newline at end of file + + + + + + UPDATE wms_structattr + SET lock_type = '4', + inv_type = '3', + inv_id = #{invId}, + inv_code = #{invCode} + WHERE deleted = '0' + AND struct_code = #{structCode} + AND lock_type = '1' + AND storagevehicle_code IS NOT NULL + AND storagevehicle_code != '' + + + + UPDATE wms_structattr + SET lock_type = '5', + inv_type = '3', + inv_id = #{invId}, + inv_code = #{invCode} + WHERE deleted = '0' + AND struct_code = #{structCode} + AND lock_type = '1' + AND (storagevehicle_code IS NULL OR storagevehicle_code = '') + + + + UPDATE wms_structattr + SET lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL + WHERE deleted = '0' + AND inv_id = #{invId} + AND lock_type IN ('4', '5') + + diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/OutboundPlacementPlannerTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/OutboundPlacementPlannerTest.java new file mode 100644 index 00000000..326751b1 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/OutboundPlacementPlannerTest.java @@ -0,0 +1,47 @@ +package cn.code.nl.module.wms.service.iostorinv; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OutboundPlacementPlannerTest { + + @Test + void shouldMoveBlockingBoxBeforeOutboundFromLeft() { + List actions = OutboundPlacementPlanner.plan("02", List.of( + new OutboundPlacementPlanner.Slot("S1", 1, "BLOCKER", false), + new OutboundPlacementPlanner.Slot("S2", 2, "TARGET", true), + new OutboundPlacementPlanner.Slot("S3", 3, null, false))); + + assertEquals(List.of( + new OutboundPlacementPlanner.Action("MOVE", "S1", "BLOCKER"), + new OutboundPlacementPlanner.Action("OUT", "S2", "TARGET")), actions); + } + + @Test + void shouldMoveBlockingBoxBeforeOutboundFromRight() { + List actions = OutboundPlacementPlanner.plan("03", List.of( + new OutboundPlacementPlanner.Slot("S1", 1, null, false), + new OutboundPlacementPlanner.Slot("S2", 2, "TARGET", true), + new OutboundPlacementPlanner.Slot("S3", 3, "BLOCKER", false))); + + assertEquals(List.of( + new OutboundPlacementPlanner.Action("MOVE", "S3", "BLOCKER"), + new OutboundPlacementPlanner.Action("OUT", "S2", "TARGET")), actions); + } + + @Test + void shouldChooseSideWithFewerBlockingBoxesForDoubleAccess() { + List actions = OutboundPlacementPlanner.plan("01", List.of( + new OutboundPlacementPlanner.Slot("S1", 1, "BLOCKER-1", false), + new OutboundPlacementPlanner.Slot("S2", 2, "BLOCKER-2", false), + new OutboundPlacementPlanner.Slot("S3", 3, "TARGET", true), + new OutboundPlacementPlanner.Slot("S4", 4, "BLOCKER-3", false))); + + assertEquals(List.of( + new OutboundPlacementPlanner.Action("MOVE", "S4", "BLOCKER-3"), + new OutboundPlacementPlanner.Action("OUT", "S3", "TARGET")), actions); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelectorTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelectorTest.java new file mode 100644 index 00000000..fd9be125 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/moveinv/MoveTargetStructSelectorTest.java @@ -0,0 +1,34 @@ +package cn.code.nl.module.wms.service.moveinv; + +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +class MoveTargetStructSelectorTest { + + @Test + void shouldSelectNearestAvailableStructAndExcludeUsedTarget() { + StrucAttrDO source = struct("S", 1, 10, 2, 1); + StrucAttrDO farther = struct("T1", 1, 15, 2, 1); + StrucAttrDO nearest = struct("T2", 1, 11, 2, 1); + StrucAttrDO used = struct("T3", 1, 9, 2, 1); + + StrucAttrDO result = MoveTargetStructSelector.select( + source, List.of(farther, nearest, used), Set.of("T3")); + + Assertions.assertEquals("T2", result.getStructId()); + } + + private StrucAttrDO struct(String id, int row, int col, int layer, int block) { + return StrucAttrDO.builder() + .structId(id) + .rowNum(row) + .colNum(col) + .layerNum(layer) + .blockNum(block) + .build(); + } +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts index 90d1c2b5..9bc437fb 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts @@ -240,6 +240,13 @@ export function manualAllocate(data: { return requestClient.post('/wms/iostor-inv/manual-allocate', data); } +export function oneClickSet(iostorinvId: string, iostorinvdtlId: string) { + return requestClient.post('/wms/iostor-inv/one-click-set', { + iostorinvId, + iostorinvdtlId, + }); +} + /** 新增出入库单主表 */ export function createIostorInv(data: WmsIostorInvApi.IostorInv) { return requestClient.post('/wms/iostor-inv/create', data); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/moveinv/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/moveinv/index.ts new file mode 100644 index 00000000..09f43a79 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/moveinv/index.ts @@ -0,0 +1,103 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace WmsMoveInvApi { + export interface Detail { + moveinvdtlId?: string; + storagevehicleCode: string; + turnoutSectCode?: string; + turnoutStructCode: string; + turninSectCode?: string; + turninStructCode: string; + materialCode?: string; + pcsn?: string; + qtyUnitId?: string; + qtyUnitName?: string; + qty: number; + sourceBillCode?: string; + remark?: string; + } + + export interface MoveInv { + moveinvId?: string; + billCode?: string; + billType: string; + bizDate: string; + storId: string; + storName?: string; + totalQty?: number; + detailCount?: number; + billStatus?: string; + remark?: string; + creatorName?: string; + createTime?: string; + details: Detail[]; + } + + export interface Inventory { + structId: string; + structCode: string; + structName: string; + sectId: string; + sectCode: string; + sectName: string; + storId: string; + storagevehicleCode: string; + layerNum: number; + materialCode?: string; + pcsn?: string; + qtyUnitId?: string; + qtyUnitName?: string; + qty: number; + sourceBillCode?: string; + } + + export interface Struct { + structId: string; + structCode: string; + structName: string; + sectCode: string; + sectName: string; + layerNum: number; + } +} + +export function getMoveInvPage(params: PageParam) { + return requestClient.get>( + '/wms/move-inv/page', + { params }, + ); +} + +export function getMoveInv(id: string) { + return requestClient.get('/wms/move-inv/get', { + params: { id }, + }); +} + +export function createMoveInv(data: WmsMoveInvApi.MoveInv) { + return requestClient.post('/wms/move-inv/create', data); +} + +export function updateMoveInv(data: WmsMoveInvApi.MoveInv) { + return requestClient.put('/wms/move-inv/update', data); +} + +export function deleteMoveInv(id: string) { + return requestClient.delete('/wms/move-inv/delete', { params: { id } }); +} + +export function getMoveAvailableInventory(storId: string, sectId?: string) { + return requestClient.get( + '/wms/move-inv/availableInventory', + { params: { storId, sectId } }, + ); +} + +export function getMoveTargetStructs(sourceStructCode: string) { + return requestClient.get( + '/wms/move-inv/availableTargetStructs', + { params: { sourceStructCode } }, + ); +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/allocation.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/allocation.vue index 908aad5c..5268eb29 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/allocation.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/allocation.vue @@ -14,6 +14,7 @@ import { cancelAllAllocations, getIostorInvDetails, getOutboundAllocations, + oneClickSet, } from '#/api/wms/iostorinv'; import { getSectAttrSimpleList } from '#/api/wms/sectattr'; @@ -204,8 +205,18 @@ async function handleManualSuccess() { emit('success'); } -function todo(name: string) { - message.info(`${name}功能待开发`); +async function handleOneClickSet() { + if (!header.value || !requireSelectedDetail()) return; + loading.value = true; + try { + await oneClickSet(header.value.iostorinvId, selectedDetailId.value!); + await loadDetails(); + await loadSelectedAllocations(); + emit('success'); + message.success('一键设置并下发任务完成'); + } finally { + loading.value = false; + } } function statusLabel(value?: string) { @@ -258,7 +269,6 @@ const [Modal, modalApi] = useVbenModal({ - @@ -280,7 +290,12 @@ const [Modal, modalApi] = useVbenModal({ -
分配明细
+
+ 分配明细 + +
+import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { WmsMoveInvApi } from '#/api/wms/moveinv'; + +import { Page, useVbenModal } from '@vben/common-ui'; + +import { Button, message } from 'antdv-next'; + +import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; +import { deleteMoveInv, getMoveInvPage } from '#/api/wms/moveinv'; + +import { useGridColumns, useGridFormSchema } from './data'; +import Detail from './modules/detail.vue'; +import Form from './modules/form.vue'; + +const [FormModal, formModalApi] = useVbenModal({ + connectedComponent: Form, + destroyOnClose: true, +}); +const [DetailModal, detailModalApi] = useVbenModal({ + connectedComponent: Detail, + destroyOnClose: true, +}); + +function refresh() { + gridApi.query(); +} + +function create() { + formModalApi.setData(null).open(); +} + +function edit(row: WmsMoveInvApi.MoveInv) { + if (row.billStatus !== '10') { + message.warning('只有生成状态的移库单允许修改'); + return; + } + formModalApi.setData(row).open(); +} + +function view(row: WmsMoveInvApi.MoveInv) { + detailModalApi.setData(row).open(); +} + +async function remove(row: WmsMoveInvApi.MoveInv) { + if (row.billStatus !== '10') { + message.warning('只有生成状态的移库单允许删除'); + return; + } + await deleteMoveInv(row.moveinvId!); + message.success('删除成功'); + refresh(); +} + +const [Grid, gridApi] = useVbenVxeGrid({ + formOptions: { schema: useGridFormSchema() }, + gridOptions: { + columns: useGridColumns(), + height: 'auto', + proxyConfig: { + ajax: { + query: async ({ page }, values) => + await getMoveInvPage({ + ...values, + pageNo: page.currentPage, + pageSize: page.pageSize, + }), + }, + }, + rowConfig: { isHover: true, keyField: 'moveinvId' }, + toolbarConfig: { refresh: true, search: true }, + } as VxeTableGridOptions, +}); + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/detail.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/detail.vue new file mode 100644 index 00000000..10abf3df --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/detail.vue @@ -0,0 +1,57 @@ + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/form.vue new file mode 100644 index 00000000..d5c07c07 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/modules/form.vue @@ -0,0 +1,296 @@ + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/order/movement/index.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/order/movement/index.vue new file mode 100644 index 00000000..465fadbd --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/order/movement/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/sql/mysql/wms_moveinv_lock_type.sql b/sql/mysql/wms_moveinv_lock_type.sql new file mode 100644 index 00000000..119453a2 --- /dev/null +++ b/sql/mysql/wms_moveinv_lock_type.sql @@ -0,0 +1,20 @@ +-- 移库仓位锁类型。按字典类型和值幂等插入。 +INSERT INTO system_dict_data + (id, sort, label, value, dict_type, status, color_type, css_class, + remark, creator, create_time, updater, update_time, deleted) +SELECT 2080566516439883781, 4, '移出锁', '4', 'wms_lock_type', 0, 'warning', '', + '移库来源仓位锁', '1', NOW(), '1', NOW(), b'0' + WHERE NOT EXISTS ( + SELECT 1 FROM system_dict_data + WHERE dict_type = 'wms_lock_type' AND value = '4' AND deleted = b'0' + ); + +INSERT INTO system_dict_data + (id, sort, label, value, dict_type, status, color_type, css_class, + remark, creator, create_time, updater, update_time, deleted) +SELECT 2080566516439883782, 5, '移入锁', '5', 'wms_lock_type', 0, 'processing', '', + '移库目标仓位锁', '1', NOW(), '1', NOW(), b'0' + WHERE NOT EXISTS ( + SELECT 1 FROM system_dict_data + WHERE dict_type = 'wms_lock_type' AND value = '5' AND deleted = b'0' + ); diff --git a/sql/mysql/wms_moveinv_menu.sql b/sql/mysql/wms_moveinv_menu.sql new file mode 100644 index 00000000..834ea780 --- /dev/null +++ b/sql/mysql/wms_moveinv_menu.sql @@ -0,0 +1,85 @@ +-- 移库管理菜单及权限。父菜单按“仓储业务”路径查找,脚本可重复执行。 +SET @wms_business_menu_id = ( + SELECT id + FROM system_menu + WHERE path = 'wms-business' + AND type = 1 + AND deleted = b'0' + LIMIT 1 +); + +INSERT INTO system_menu + (id, name, permission, type, sort, parent_id, path, icon, component, + component_name, status, visible, keep_alive, always_show, + creator, create_time, updater, update_time, deleted) +SELECT 6470, '移库管理', '', 2, 1, @wms_business_menu_id, 'move-inv', 'ep:sort', + 'wms/moveinv/index', 'WmsMoveInv', 0, b'1', b'1', b'1', + '1', NOW(), '1', NOW(), b'0' + WHERE @wms_business_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM system_menu + WHERE path = 'move-inv' + AND parent_id = @wms_business_menu_id + AND deleted = b'0' + ); + +SET @move_inv_menu_id = ( + SELECT id + FROM system_menu + WHERE path = 'move-inv' + AND parent_id = @wms_business_menu_id + AND deleted = b'0' + LIMIT 1 +); + +INSERT INTO system_menu + (id, name, permission, type, sort, parent_id, path, icon, component, + component_name, status, visible, keep_alive, always_show, + creator, create_time, updater, update_time, deleted) +SELECT 6471, '移库单查询', 'wms:movement-order:query', 3, 1, @move_inv_menu_id, + '', '#', NULL, NULL, 0, b'1', b'1', b'1', + '1', NOW(), '1', NOW(), b'0' + WHERE @move_inv_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM system_menu + WHERE permission = 'wms:movement-order:query' AND deleted = b'0' + ); + +INSERT INTO system_menu + (id, name, permission, type, sort, parent_id, path, icon, component, + component_name, status, visible, keep_alive, always_show, + creator, create_time, updater, update_time, deleted) +SELECT 6472, '移库单创建', 'wms:movement-order:create', 3, 2, @move_inv_menu_id, + '', '#', NULL, NULL, 0, b'1', b'1', b'1', + '1', NOW(), '1', NOW(), b'0' + WHERE @move_inv_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM system_menu + WHERE permission = 'wms:movement-order:create' AND deleted = b'0' + ); + +INSERT INTO system_menu + (id, name, permission, type, sort, parent_id, path, icon, component, + component_name, status, visible, keep_alive, always_show, + creator, create_time, updater, update_time, deleted) +SELECT 6473, '移库单更新', 'wms:movement-order:update', 3, 3, @move_inv_menu_id, + '', '#', NULL, NULL, 0, b'1', b'1', b'1', + '1', NOW(), '1', NOW(), b'0' + WHERE @move_inv_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM system_menu + WHERE permission = 'wms:movement-order:update' AND deleted = b'0' + ); + +INSERT INTO system_menu + (id, name, permission, type, sort, parent_id, path, icon, component, + component_name, status, visible, keep_alive, always_show, + creator, create_time, updater, update_time, deleted) +SELECT 6474, '移库单删除', 'wms:movement-order:delete', 3, 4, @move_inv_menu_id, + '', '#', NULL, NULL, 0, b'1', b'1', b'1', + '1', NOW(), '1', NOW(), b'0' + WHERE @move_inv_menu_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM system_menu + WHERE permission = 'wms:movement-order:delete' AND deleted = b'0' + ); diff --git a/sql/mysql/wms_one_click_set_test_data.sql b/sql/mysql/wms_one_click_set_test_data.sql new file mode 100644 index 00000000..6500446b --- /dev/null +++ b/sql/mysql/wms_one_click_set_test_data.sql @@ -0,0 +1,90 @@ +-- 出库一键设置测试数据。 +-- 仓库:遂宁成品仓;库区:主存区。 +-- 所有编码均使用 TEST-OCS- 前缀,脚本使用 INSERT IGNORE,可重复执行且不会覆盖测试中的锁状态。 + +INSERT IGNORE INTO wms_structattr + (struct_id, struct_code, struct_name, sect_id, sect_code, sect_name, + stor_id, stor_code, stor_name, is_tempstruct, row_num, col_num, layer_num, + block_num, placement_type, storagevehicle_code, storagevehicle_qty, + lock_type, creator, updater, deleted, is_used) +VALUES + -- 左侧出库:从小列号侧出库,目标箱前有一个挡路箱。 + ('2080900000000001001', 'TEST-OCS-L-01', '左侧挡路仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 91, 1, 1, 91, '02', + 'TEST-OCS-L-BLOCK', 1, '1', '1', '1', '0', 1), + ('2080900000000001002', 'TEST-OCS-L-02', '左侧目标仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 91, 2, 1, 91, '02', + 'TEST-OCS-L-TARGET', 1, '1', '1', '1', '0', 1), + + -- 右侧出库:从大列号侧出库,目标箱前有一个挡路箱。 + ('2080900000000001101', 'TEST-OCS-R-01', '右侧目标仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 92, 1, 1, 92, '03', + 'TEST-OCS-R-TARGET', 1, '1', '1', '1', '0', 1), + ('2080900000000001102', 'TEST-OCS-R-02', '右侧挡路仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 92, 2, 1, 92, '03', + 'TEST-OCS-R-BLOCK', 1, '1', '1', '1', '0', 1), + + -- 双向出库:目标箱两侧均有挡路箱,左右移动数相同时默认选择左侧。 + ('2080900000000001201', 'TEST-OCS-D-01', '双向左挡路仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 93, 1, 1, 93, '01', + 'TEST-OCS-D-LBLOCK', 1, '1', '1', '1', '0', 1), + ('2080900000000001202', 'TEST-OCS-D-02', '双向目标仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 93, 2, 1, 93, '01', + 'TEST-OCS-D-TARGET', 1, '1', '1', '1', '0', 1), + ('2080900000000001203', 'TEST-OCS-D-03', '双向右挡路仓位', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 93, 3, 1, 93, '01', + 'TEST-OCS-D-RBLOCK', 1, '1', '1', '1', '0', 1), + + -- 空闲移库目的仓位。 + ('2080900000000001301', 'TEST-OCS-FREE-01', '移库空位01', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 94, 1, 1, 94, '01', + NULL, 0, '1', '1', '1', '0', 1), + ('2080900000000001302', 'TEST-OCS-FREE-02', '移库空位02', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 94, 2, 1, 94, '01', + NULL, 0, '1', '1', '1', '0', 1), + ('2080900000000001303', 'TEST-OCS-FREE-03', '移库空位03', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 94, 3, 1, 94, '01', + NULL, 0, '1', '1', '1', '0', 1), + ('2080900000000001304', 'TEST-OCS-FREE-04', '移库空位04', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 94, 4, 1, 94, '01', + NULL, 0, '1', '1', '1', '0', 1), + ('2080900000000001305', 'TEST-OCS-FREE-05', '移库空位05', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 94, 5, 1, 94, '01', + NULL, 0, '1', '1', '1', '0', 1), + ('2080900000000001306', 'TEST-OCS-FREE-06', '移库空位06', '2078406241102450689', 'ZC01', '主存区', + '2078405279260463106', 'AC01', '遂宁成品仓', '0', 94, 6, 1, 94, '01', + NULL, 0, '1', '1', '1', '0', 1); + +INSERT IGNORE INTO wms_group_plate + (group_id, vehicle_code, status, material_id, pcsn, qty, frozen_qty, + qty_unit_id, qty_unit_name, remark, ext_code, ext_type, ext_dtl_code, + material_code, creator, updater, deleted) +VALUES + -- 挡路库存统一使用测试物料 B,避免被目标出库明细误分配。 + (2080900000000002001, 'TEST-OCS-L-BLOCK', '02', '2080700000000000102', + 'OCS-L-BLOCK-PCSN', 50.000, 0.000, 'KG', '千克', '左侧出库挡路箱', + 'OCS-BLOCK-SOURCE', 'TEST', 'OCS-L-BLOCK-DTL', 'TEST-MAT-002', '1', '1', b'0'), + (2080900000000002002, 'TEST-OCS-L-TARGET', '02', '2080700000000000101', + 'OCS-L-TARGET-PCSN', 50.000, 0.000, 'KG', '千克', '左侧出库目标箱', + 'OCS-L-SOURCE', 'TEST', 'OCS-L-TARGET-DTL', 'TEST-MAT-001', '1', '1', b'0'), + + (2080900000000002101, 'TEST-OCS-R-TARGET', '02', '2080700000000000101', + 'OCS-R-TARGET-PCSN', 50.000, 0.000, 'KG', '千克', '右侧出库目标箱', + 'OCS-R-SOURCE', 'TEST', 'OCS-R-TARGET-DTL', 'TEST-MAT-001', '1', '1', b'0'), + (2080900000000002102, 'TEST-OCS-R-BLOCK', '02', '2080700000000000102', + 'OCS-R-BLOCK-PCSN', 50.000, 0.000, 'KG', '千克', '右侧出库挡路箱', + 'OCS-BLOCK-SOURCE', 'TEST', 'OCS-R-BLOCK-DTL', 'TEST-MAT-002', '1', '1', b'0'), + + (2080900000000002201, 'TEST-OCS-D-LBLOCK', '02', '2080700000000000102', + 'OCS-D-LBLOCK-PCSN', 50.000, 0.000, 'KG', '千克', '双向出库左挡路箱', + 'OCS-BLOCK-SOURCE', 'TEST', 'OCS-D-LBLOCK-DTL', 'TEST-MAT-002', '1', '1', b'0'), + (2080900000000002202, 'TEST-OCS-D-TARGET', '02', '2080700000000000101', + 'OCS-D-TARGET-PCSN', 50.000, 0.000, 'KG', '千克', '双向出库目标箱', + 'OCS-D-SOURCE', 'TEST', 'OCS-D-TARGET-DTL', 'TEST-MAT-001', '1', '1', b'0'), + (2080900000000002203, 'TEST-OCS-D-RBLOCK', '02', '2080700000000000102', + 'OCS-D-RBLOCK-PCSN', 50.000, 0.000, 'KG', '千克', '双向出库右挡路箱', + 'OCS-BLOCK-SOURCE', 'TEST', 'OCS-D-RBLOCK-DTL', 'TEST-MAT-002', '1', '1', b'0'); + +-- 如需清理,请先删除引用这些载具的测试出库分配/任务,再执行: +-- DELETE FROM wms_group_plate WHERE vehicle_code LIKE 'TEST-OCS-%'; +-- DELETE FROM wms_structattr WHERE struct_code LIKE 'TEST-OCS-%';