From c15e9a37dd4b70cc8b5133a9776ac20078e7765f Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:16:38 +0800 Subject: [PATCH 01/24] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-22-outbound-create-design.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-22-outbound-create-design.md diff --git a/docs/superpowers/specs/2026-07-22-outbound-create-design.md b/docs/superpowers/specs/2026-07-22-outbound-create-design.md new file mode 100644 index 00000000..bf815e8f --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-outbound-create-design.md @@ -0,0 +1,117 @@ +# 出库单新增功能设计 + +## 目标与范围 + +按照 `.image/出库新增.png` 重构出入库单主表的新增布局,支持从仓库可用库存选择和手工新增物料汇总两种明细添加方式,并将主表与明细作为一个聚合事务保存。 + +本次仅实现出库新增,不新增数据库字段,不扩展入库、明细编辑、分配、确认或回传流程。主表使用 `wms_iostorinv`,明细写入 `wms_iostorinvdtl`。 + +## 页面布局 + +新增页采用已确认的参考图布局: + +- 顶部操作区提供“保存”和“关闭”。 +- 主表区域展示单据号、仓库、业务类型、单据状态、明细数、总重量、业务日期和备注。 +- 明细区域提供“选择库存”和“新增汇总”两个入口。 +- 明细表展示序号、物料编码、物料名称、箱号、子卷号、SAP 批次号、出库重量、单位、源单号、明细备注和操作。 +- 初次打开时明细表为空。 + +## 主表字段与默认值 + +| 页面字段 | `wms_iostorinv` 字段 | 行为 | +| --- | --- | --- | +| 单据号 | `billCode` | 只读;保存时通过 base 模块编码生成 API 生成 | +| 仓库 | `storId`、`storCode`、`storName` | 必填,可选择 | +| 业务类型 | `billType` | 必填,可选择 | +| 出入类型 | `ioType` | 后端固定为出库 | +| 单据状态 | `billStatus` | 默认并固定为“生成” | +| 明细数 | `detailCount` | 根据明细条数自动计算 | +| 总重量 | `totalWeight` | 汇总明细的出库重量 | +| 业务日期 | `bizDate` | 默认当天,可修改 | +| 备注 | `remark` | 选填 | + +前端在明细增删和重量变化时即时更新明细数与总重量。后端保存时必须根据请求明细重新计算,不能直接信任前端汇总字段。 + +## 明细字段与持久化边界 + +明细使用 `wms_iostorinvdtl` 的现有字段: + +| 业务含义 | 明细字段 | 保存规则 | +| --- | --- | --- | +| 主表关联 | `iostorinvId` | 主表插入后回填 | +| 序号 | `seqNo` | 从 1 开始连续生成 | +| 物料 | `materialId`、`materialCode` | 必填 | +| 子卷号 | `pcsn` | 库存明细带入;手工汇总为空 | +| 出库重量 | `planQty` | 必须大于 0 | +| 已分配数量 | `assignQty` | 新增时为 0 | +| 未分配数量 | `unassignQty` | 新增时等于出库重量 | +| 单位 | `qtyUnitId`、`qtyUnitName` | 从库存或物料信息带入 | +| 来源单据 | `sourceBillCode`、`sourceBillType`、`sourceBilldtlId` | 库存存在来源时带入 | +| 备注 | `remark` | 选填 | + +箱号、物料名称和独立 SAP 批次号在 `wms_iostorinvdtl` 中没有对应字段,只用于新增页面展示,不落库,也不与其他字段混用。手工汇总明细的箱号、子卷号和 SAP 批次号均为空。 + +## 明细新增方式 + +### 从库存选择 + +1. 用户必须先选择仓库。 +2. 库存弹窗查询该仓库下的可用库存,并支持按物料、箱号、子卷号等现有库存字段筛选。 +3. 用户可独立勾选库存记录,界面不自动联动勾选同箱的其他子卷。 +4. 确认时以后端为准,提取已选记录涉及的箱号,去重后重新查询这些箱号在所选仓库下的全部可用子卷。 +5. 将查询出的每个子卷逐条带回出库明细表;默认出库重量为该子卷当前可用重量。 +6. 同一箱号重复选择只展开一次,避免重复明细。 + +### 新增物料汇总 + +1. 用户选择物料并输入总出库重量,可填写备注。 +2. 单位从物料现有信息带出。 +3. 新增一条汇总明细,箱号、子卷号和 SAP 批次号保持为空。 + +## 接口与事务设计 + +采用主表与明细聚合保存方案: + +- 新增“创建出库单(含明细)”接口,接收主表可编辑字段和明细列表。 +- 新增按仓库和筛选条件查询可用库存的接口。 +- 库存确认或保存前由后端按箱号展开全部可用子卷,确保展开结果属于当前仓库且仍然可用。 +- 保存服务开启事务,先调用 base 模块编码生成 API 获取单据号,再构造主表、计算汇总、插入主表,最后插入全部 `wms_iostorinvdtl` 明细。 +- 编码生成、主表插入或任一明细插入失败时整体回滚。 + +单据号规则沿用项目已有的出入库编码规则配置和 `CodeGenApi` 调用方式,不在本功能中创建新的编码规则或字段。 + +## 数据校验与错误处理 + +- 仓库、业务类型和业务日期必填。 +- 保存时至少存在一条明细。 +- 每条明细必须包含物料,且出库重量大于 0。 +- 切换仓库时清空当前明细,避免跨仓库数据混用。 +- 库存带入时校验箱号属于所选仓库,并以当前可用库存重新展开;库存已不可用时返回明确的业务错误。 +- 重复箱号在展开前去重。 +- 编码 API 返回失败或空编码时终止保存并回滚。 +- 后端固定出库类型和生成状态,并重新计算明细数、总重量、序号及初始分配数量,防止客户端篡改。 + +## 测试与验收 + +后端测试覆盖: + +- base 编码 API 生成的单据号写入主表。 +- 默认出库类型和生成状态正确。 +- 后端根据明细重算明细数和总重量。 +- 主表与明细在同一事务中写入,异常时整体回滚。 +- 库存查询限定所选仓库和可用数量。 +- 任意选中一个箱号下的记录后,返回该箱号全部可用子卷。 +- 多条选中记录包含重复箱号时只展开一次。 +- 物料缺失、重量非正数、空明细及失效库存被拒绝。 + +前端测试或可自动化验证覆盖: + +- 新增页业务日期默认为当天,状态为生成,明细为空。 +- 明细增删和重量修改会更新明细数与总重量。 +- 未选择仓库不能打开库存选择流程。 +- 库存确认结果逐子卷进入明细表。 +- 手工汇总行的箱号、子卷号和 SAP 批次号为空。 +- 切换仓库会清空现有明细。 +- 提交数据只使用现有主表和明细字段。 + +完成后运行 WMS 模块相关单元测试、前端类型检查和构建,并对照参考图检查新增页布局与两种明细添加流程。 From d49f722bda673bcf79be368593dad436b4a6b20b Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:23:13 +0800 Subject: [PATCH 02/24] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E6=96=B0=E5=A2=9E=E5=AE=9E=E7=8E=B0=E8=AE=A1?= =?UTF-8?q?=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-07-22-outbound-create-plan.md | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-22-outbound-create-plan.md diff --git a/docs/superpowers/plans/2026-07-22-outbound-create-plan.md b/docs/superpowers/plans/2026-07-22-outbound-create-plan.md new file mode 100644 index 00000000..81aa2718 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-outbound-create-plan.md @@ -0,0 +1,410 @@ +# 出库单新增功能实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 按确认的布局实现出库单新增页,通过库存箱号展开全部可用子卷或手工新增物料汇总,并事务保存 `wms_iostorinv` 主表和 `wms_iostorinvdtl` 明细。 + +**架构:** 前端由主弹窗、库存选择弹窗、手工汇总弹窗组成,页面只维护现有表字段和临时展示字段。后端提供可用库存查询/箱号展开接口与聚合创建接口;聚合服务调用 base 编码 API、重新计算汇总值,并在一个事务内写入主表和明细。 + +**技术栈:** Java 17、Spring Boot、MyBatis XML、OpenFeign、Vue 3、TypeScript、Ant Design Vue Next、Vben Form、Vxe Grid、Vitest + +--- + +## 文件结构 + +### 后端 + +- 修改 `nl-module-wms/nl-module-wms-server/pom.xml`:引入 `nl-module-base-api`。 +- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java`:注册 `CodeGenApi` Feign 客户端。 +- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java`:聚合创建请求及内部明细对象。 +- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryPageReqVO.java`:库存筛选与分页参数。 +- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java`:库存与页面展示结果。 +- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java`:仓库和已选箱号集合。 +- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java`:声明库存查询与按箱号展开方法。 +- 修改 `nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml`:实现跨仓库结构和组盘库存的 SQL。 +- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java`:声明库存查询、展开和聚合创建方法。 +- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java`:实现校验、编码生成、汇总和事务写入。 +- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java`:暴露三个管理端接口。 +- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java`:增加编码和库存失效错误码。 +- 创建 `nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java`:Spring 集成测试。 + +### 前端 + +- 修改 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts`:增加聚合请求、库存结果类型和接口函数。 +- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts`:纯函数计算明细数、总重量和去重键。 +- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts`:汇总联动的红绿测试。 +- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue`:库存筛选、多选和确认展开。 +- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue`:物料汇总新增。 +- 修改 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue`:实现确认布局、明细联动和聚合提交。 + +--- + +## 任务 1:建立后端请求契约与 RPC 依赖 + +- [ ] **步骤 1:先创建 Spring 集成测试骨架并验证当前编译失败** + +在 `IostorInvServiceLocalSpringTest.java` 中使用 `@SpringBootTest`、`@ActiveProfiles("local")` 和 `@Resource IostorInvService`,先引用尚不存在的 `IostorInvCreateReqVO` 与 `createOutbound`: + +```java +@SpringBootTest +@ActiveProfiles("local") +class IostorInvServiceLocalSpringTest { + @Resource + private IostorInvService iostorInvService; + + @Test + void createOutboundRejectsEmptyDetails() { + IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO(); + Assertions.assertThrows(ConstraintViolationException.class, + () -> iostorInvService.createOutbound(reqVO)); + } +} +``` + +运行: + +```bash +mvn -pl nl-module-wms/nl-module-wms-server -am -DskipTests compile +``` + +预期:FAIL,提示 `IostorInvCreateReqVO` 或 `createOutbound` 不存在。 + +- [ ] **步骤 2:增加 base API 依赖和 Feign 注册** + +在 WMS server 的 `pom.xml` 增加: + +```xml + + cn.nl.cloud + nl-module-base-api + ${revision} + +``` + +创建 `RpcConfiguration`,使用 `@Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false)` 与 `@EnableFeignClients(clients = CodeGenApi.class)`。 + +- [ ] **步骤 3:创建带注解校验的聚合请求对象** + +`IostorInvCreateReqVO` 顶层包含 `@NotEmpty billType`、`@NotEmpty storId`、`@NotNull bizDate`、`remark`、`@NotEmpty @Valid List details`;内部 `Detail` 包含: + +```java +@NotEmpty(message = "物料编码不能为空") +private String materialCode; +private String materialId; +private String pcsn; +@NotNull(message = "出库重量不能为空") +@DecimalMin(value = "0.001", message = "出库重量必须大于0") +private BigDecimal planQty; +private String qtyUnitId; +private String qtyUnitName; +private String sourceBillCode; +private String sourceBillType; +private String sourceBilldtlId; +private String remark; +``` + +不接收 `billCode`、`billStatus`、`detailCount`、`totalWeight` 和 `seqNo`,这些字段全部由服务端生成。 + +- [ ] **步骤 4:声明 Service 方法并验证测试从编译失败进入校验失败/通过** + +```java +@Validated +public interface IostorInvService { + String createOutbound(@Valid IostorInvCreateReqVO reqVO); +} +``` + +运行: + +```bash +mvn -pl nl-module-wms/nl-module-wms-server -am -Dtest=IostorInvServiceLocalSpringTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +预期:测试能够启动且空请求因参数校验被拒绝;若 local 数据源不可用,保留测试并记录环境阻塞,继续用编译验证生产契约。 + +- [ ] **步骤 5:提交契约变更** + +```bash +git add nl-module-wms/nl-module-wms-server/pom.xml nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +git commit -m "feat: 定义出库单聚合创建契约" +``` + +## 任务 2:实现可用库存查询和按箱号展开 + +- [ ] **步骤 1:扩展失败测试,定义箱号展开行为** + +在集成测试准备同一仓库下箱号 `BOX-001` 的两个可用子卷和另一个仓库的同箱数据,断言: + +```java +List rows = iostorInvService.expandAvailableInventory( + "STOR-001", List.of("BOX-001", "BOX-001")); +Assertions.assertEquals(2, rows.size()); +Assertions.assertTrue(rows.stream().allMatch(row -> "BOX-001".equals(row.getVehicleCode()))); +``` + +运行指定测试,预期:FAIL,方法和响应对象尚不存在。 + +- [ ] **步骤 2:创建查询参数和响应对象** + +`AvailableInventoryPageReqVO extends PageParam`,字段为 `@NotEmpty storId`、`materialCode`、`vehicleCode`、`pcsn`;`AvailableInventoryRespVO` 使用库存现有字段:`vehicleCode`、`pcsn`、`materialId`、`materialCode`、`materialName`(仅返回展示)、`availableQty`、单位、来源字段及仓库标识。`ExpandAvailableInventoryReqVO` 包含 `@NotEmpty storId` 和 `@NotEmpty List vehicleCodes`。 + +- [ ] **步骤 3:声明 Mapper 方法** + +```java +PageResult selectAvailableInventoryPage(AvailableInventoryPageReqVO reqVO); + +List selectAvailableInventoryByVehicleCodes( + @Param("storId") String storId, + @Param("vehicleCodes") Collection vehicleCodes); +``` + +- [ ] **步骤 4:在 XML 实现统一库存 SQL** + +基础关联为 `wms_group_plate gp INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0`,固定条件为 `sa.stor_id = #{storId}`、`gp.status = '可用'`、`gp.qty - gp.frozen_qty > 0` 和 WMS 两表 `is_deleted = 0`。分页查询增加物料、箱号、子卷号筛选;展开查询使用 `` 的箱号集合。结果中 `available_qty = gp.qty - gp.frozen_qty`,物料名称从 `mb.material_name` 返回但不写入明细表。 + +- [ ] **步骤 5:Service 对箱号去重后查询全部子卷** + +```java +List distinctVehicleCodes = vehicleCodes.stream().distinct().toList(); +return iostorinvDtlMapper.selectAvailableInventoryByVehicleCodes(storId, distinctVehicleCodes); +``` + +控制器提供: + +```java +@GetMapping("/availableInventoryPage") +public CommonResult> getAvailableInventoryPage( + @Valid AvailableInventoryPageReqVO reqVO) + +@PostMapping("/expandAvailableInventory") +public CommonResult> expandAvailableInventory( + @Valid @RequestBody ExpandAvailableInventoryReqVO reqVO) +``` + +- [ ] **步骤 6:运行集成测试和 Mapper 编译** + +```bash +mvn -pl nl-module-wms/nl-module-wms-server -am -Dtest=IostorInvServiceLocalSpringTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +预期:同箱两子卷全部返回、重复箱号不产生重复行、其他仓库数据不返回。 + +- [ ] **步骤 7:提交库存查询变更** + +```bash +git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +git commit -m "feat: 支持按箱号展开可用库存子卷" +``` + +## 任务 3:实现主表与明细事务保存 + +- [ ] **步骤 1:补充失败测试** + +构造两条重量 `12.500`、`7.250` 的明细,创建后查询数据库并断言:主表 `ioType = "OUT"`、`billStatus = "生成"`、`detailCount = 2`、`totalWeight = 19.750`;明细序号为 1/2、`assignQty = 0`、`unassignQty = planQty`。测试请求不传任何汇总字段。 + +- [ ] **步骤 2:实现编码生成和事务写入** + +`createOutbound` 标记 `@Transactional(rollbackFor = Exception.class)`: + +1. 构造 `CodeGenerateReqDTO`,设置项目现有出入库规则编码 `IO_CODE`。 +2. 调用 `CodeGenApi.generate`,检查 `CommonResult.isSuccess()` 且编码非空;失败抛出中文业务异常。 +3. 手工逐字段创建 `IostorInvDO`,固定 `ioType = "OUT"`、`billStatus = "生成"`。 +4. 使用 `details.stream().map(Detail::getPlanQty).reduce(BigDecimal.ZERO, BigDecimal::add)` 计算总重量并设置明细数。 +5. 插入主表后,循环构造 `IostorinvDtlDO`,写入关联 ID、连续序号、物料、`pcsn`、计划重量、分配数量、单位、来源和备注。 + +- [ ] **步骤 3:增加控制器端点和错误码** + +```java +@PostMapping("/createOutbound") +@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") +public CommonResult createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) { + return success(iostorInvService.createOutbound(reqVO)); +} +``` + +错误码仅增加“单据号生成失败”和“所选库存已不可用”,不新增数据库字段或表。 + +- [ ] **步骤 4:验证红绿与事务回滚** + +运行集成测试;再让测试用编码 API 失败配置触发异常,断言主表和明细数量均未增加。 + +- [ ] **步骤 5:提交聚合保存** + +```bash +git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +git commit -m "feat: 事务保存出库单及明细" +``` + +## 任务 4:扩展前端 API 与明细汇总纯函数 + +- [ ] **步骤 1:先写失败的 Vitest 测试** + +```ts +import { describe, expect, it } from 'vitest'; +import { summarizeDetails } from './detail-summary'; + +describe('summarizeDetails', () => { + it('按明细条数和出库重量计算汇总', () => { + expect(summarizeDetails([{ planQty: 12.5 }, { planQty: 7.25 }])).toEqual({ + detailCount: 2, + totalWeight: 19.75, + }); + }); +}); +``` + +运行: + +```bash +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom +``` + +预期:FAIL,模块不存在。 + +- [ ] **步骤 2:实现最小汇总函数** + +```ts +export function summarizeDetails(details: Array<{ planQty?: number }>) { + return { + detailCount: details.length, + totalWeight: details.reduce((sum, item) => sum + Number(item.planQty || 0), 0), + }; +} +``` + +- [ ] **步骤 3:在 API 层增加明确类型和三个接口** + +定义 `OutboundDetail`、`OutboundCreateReq`、`AvailableInventory`、`AvailableInventoryPageReq`,并增加: + +```ts +export const getAvailableInventoryPage = (params: AvailableInventoryPageReq) => + requestClient.get>('/wms/iostor-inv/availableInventoryPage', { params }); +export const expandAvailableInventory = (storId: string, vehicleCodes: string[]) => + requestClient.post('/wms/iostor-inv/expandAvailableInventory', { storId, vehicleCodes }); +export const createOutbound = (data: OutboundCreateReq) => + requestClient.post('/wms/iostor-inv/createOutbound', data); +``` + +- [ ] **步骤 4:运行 Vitest 和类型检查** + +```bash +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck +``` + +预期:汇总测试通过,新增 API 类型无错误。 + +- [ ] **步骤 5:提交前端基础能力** + +```bash +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts +git commit -m "feat: 增加出库单前端聚合接口与汇总逻辑" +``` + +## 任务 5:实现库存选择与手工汇总弹窗 + +- [ ] **步骤 1:实现库存选择弹窗** + +`inventory-select.vue` 接收主弹窗传入的 `storId`,表单筛选物料编码、箱号、子卷号,Vxe Grid 使用服务端分页和多选。确认时只收集选中行的 `vehicleCode`,调用 `expandAvailableInventory`;返回结果逐子卷映射为明细,`planQty` 默认为 `availableQty`。勾选过程不自动修改其他子卷的选中状态。 + +- [ ] **步骤 2:实现手工汇总弹窗** + +复用 `views/base/materialbase/components/MaterialSelectModal.vue` 选择一个物料,填写大于 0 的总出库重量和备注;确认对象必须显式包含: + +```ts +{ + materialId, + materialCode, + materialName, + planQty, + qtyUnitId, + qtyUnitName, + vehicleCode: undefined, + pcsn: undefined, + sapBatchNo: undefined, + remark, +} +``` + +- [ ] **步骤 3:运行前端类型检查** + +```bash +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck +``` + +预期:两个弹窗 props、emit、API 结果映射均无类型错误。 + +- [ ] **步骤 4:提交两个弹窗** + +```bash +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue +git commit -m "feat: 增加出库库存选择和手工汇总弹窗" +``` + +## 任务 6:按参考图重构新增主弹窗 + +- [ ] **步骤 1:实现主表布局与默认值** + +重构 `form.vue` 为宽屏弹窗:单据号显示“保存时自动生成”;仓库和业务类型为必选下拉;状态只读“生成”;业务日期使用 `dayjs()` 默认为当天;明细数和总重量只读;备注跨列。仓库使用 `getBsrealStorAttrSimpleList`,业务类型从现有字典缓存中过滤出库业务类型。 + +- [ ] **步骤 2:实现明细表与两种入口** + +列顺序严格按确认原型:序号、物料编码、物料名称、箱号、子卷号、SAP 批次号、出库重量、单位、源单号、明细备注、操作。出库重量可编辑;删除、库存返回、手工汇总返回均调用 `summarizeDetails` 更新只读汇总。 + +- [ ] **步骤 3:实现仓库切换和提交** + +仓库值发生实际变化且明细非空时清空明细。提交只发送 `billType`、`storId`、`bizDate`、`remark` 及 `wms_iostorinvdtl` 可持久化字段;不得发送箱号、物料名称、SAP 批次号、单据号、状态和汇总字段。 + +- [ ] **步骤 4:运行前端测试、类型检查与构建** + +```bash +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next build +``` + +预期:测试、类型检查和生产构建均以退出码 0 完成。 + +- [ ] **步骤 5:提交主弹窗** + +```bash +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue +git commit -m "feat: 按参考布局实现出库单新增页" +``` + +## 任务 7:端到端核对与最终验证 + +- [ ] **步骤 1:核对数据库字段边界** + +检查最终 diff,确认没有 DDL、迁移脚本或 DO 新字段;前端展示字段没有进入 `createOutbound` 请求;所有明细只通过 `IostorinvDtlMapper` 写入 `wms_iostorinvdtl`。 + +- [ ] **步骤 2:运行后端完整模块验证** + +```bash +mvn -pl nl-module-wms/nl-module-wms-server -am clean test -DskipTests=false +``` + +预期:WMS server 及依赖模块编译成功,测试失败数为 0。 + +- [ ] **步骤 3:运行前端完整验证** + +```bash +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck +pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next build +``` + +预期:三条命令均退出码 0。 + +- [ ] **步骤 4:人工验收关键流程** + +启动前后端后确认:新增页默认值正确;未选仓库不能选择库存;选择任一库存记录会按箱号带回全部子卷;勾选本身不联动;手工汇总的箱号、子卷号、SAP 批次号为空;明细汇总实时变化;切换仓库清空明细;保存后主表和全部明细同时存在。 + +- [ ] **步骤 5:提交验证阶段必要修正** + +仅当验证产生修正时提交相关文件: + +```bash +git add nl-module-wms nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv +git commit -m "fix: 完善出库单新增流程验证问题" +``` From 5a59e9e07ba911034650f92f4abd83a94e0b5636 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:25:07 +0800 Subject: [PATCH 03/24] =?UTF-8?q?chore:=20=E5=BF=BD=E7=95=A5=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=20worktree=20=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ef84ab63..e86dab25 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,7 @@ application-my.yaml /nl-ui-app/unpackage/ .DS_Store -**/.DS_Store \ No newline at end of file +**/.DS_Store + +# Local Git worktrees +.worktrees/ From 65f25655e7d860623d57932424d9c9b71bc5cab0 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:28:41 +0800 Subject: [PATCH 04/24] =?UTF-8?q?feat:=20=E5=AE=9A=E4=B9=89=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E8=81=9A=E5=90=88=E5=88=9B=E5=BB=BA=E5=A5=91?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nl-module-wms/nl-module-wms-server/pom.xml | 7 +- .../iostorinv/vo/IostorInvCreateReqVO.java | 75 +++++++++++++++++++ .../rpc/config/RpcConfiguration.java | 10 +++ .../service/iostorinv/IostorInvService.java | 8 ++ .../iostorinv/IostorInvServiceImpl.java | 5 ++ .../IostorInvServiceLocalSpringTest.java | 25 +++++++ 6 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java create mode 100644 nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java diff --git a/nl-module-wms/nl-module-wms-server/pom.xml b/nl-module-wms/nl-module-wms-server/pom.xml index ef12b750..4142363f 100644 --- a/nl-module-wms/nl-module-wms-server/pom.xml +++ b/nl-module-wms/nl-module-wms-server/pom.xml @@ -42,6 +42,11 @@ nl-module-task-api ${revision} + + cn.nl.cloud + nl-module-base-api + ${revision} + @@ -146,4 +151,4 @@ - \ No newline at end of file + diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java new file mode 100644 index 00000000..7e8a6f60 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java @@ -0,0 +1,75 @@ +package cn.code.nl.module.wms.controller.admin.iostorinv.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 出库单新增 Request VO") +@Data +public class IostorInvCreateReqVO { + + @Schema(description = "单据类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "单据类型不能为空") + private String billType; + + @Schema(description = "仓库标识", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "仓库标识不能为空") + private String storId; + + @Schema(description = "业务日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "业务日期不能为空") + private LocalDateTime bizDate; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "出库单明细", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "出库单明细不能为空") + @Valid + private List details; + + @Schema(description = "出库单明细") + @Data + public static class Detail { + + @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "物料编码不能为空") + private String materialCode; + + @Schema(description = "物料标识") + private String materialId; + + @Schema(description = "批次序列号") + private String pcsn; + + @Schema(description = "计划数量", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "计划数量不能为空") + @DecimalMin(value = "0.001", message = "计划数量不能小于 0.001") + private BigDecimal planQty; + + @Schema(description = "数量单位标识") + private String qtyUnitId; + + @Schema(description = "数量单位名称") + private String qtyUnitName; + + @Schema(description = "来源单据编号") + private String sourceBillCode; + + @Schema(description = "来源单据类型") + private String sourceBillType; + + @Schema(description = "来源单据明细标识") + private String sourceBilldtlId; + + @Schema(description = "备注") + private String remark; + } +} 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 new file mode 100644 index 00000000..7fb84cdf --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,10 @@ +package cn.code.nl.module.wms.framework.rpc.config; + +import cn.code.nl.module.base.api.codegen.CodeGenApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false) +@EnableFeignClients(clients = CodeGenApi.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 42203b6d..68b722bf 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 @@ -14,6 +14,14 @@ import cn.code.nl.framework.common.pojo.PageParam; */ public interface IostorInvService { + /** + * 创建出库单 + * + * @param reqVO 出库单创建信息 + * @return 出库单编号 + */ + String createOutbound(@Valid IostorInvCreateReqVO 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 f6d75cb2..bcb984be 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 @@ -32,6 +32,11 @@ public class IostorInvServiceImpl implements IostorInvService { @Resource private IostorInvMapper iostorInvMapper; + @Override + public String createOutbound(IostorInvCreateReqVO reqVO) { + throw new UnsupportedOperationException("出库单聚合保存功能尚未实现"); + } + @Override public String createIostorInv(IostorInvSaveReqVO createReqVO) { // 插入 diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java new file mode 100644 index 00000000..b4e63bb7 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -0,0 +1,25 @@ +package cn.code.nl.module.wms.service.iostorinv; + +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolationException; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@ActiveProfiles("local") +class IostorInvServiceLocalSpringTest { + + @Resource + private IostorInvService iostorInvService; + + @Test + void createOutboundShouldRejectEmptyRequest() { + IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO(); + + assertThrows(ConstraintViolationException.class, () -> iostorInvService.createOutbound(reqVO)); + } +} From eb35c999661a0466418d6a4cd96d9cdcedf3e832 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:32:11 +0800 Subject: [PATCH 05/24] =?UTF-8?q?test:=20=E6=94=B6=E7=B4=A7=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E5=88=9B=E5=BB=BA=E5=A5=91=E7=BA=A6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/iostorinv/vo/IostorInvCreateReqVO.java | 6 +++--- .../IostorInvServiceLocalSpringTest.java | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java index 7e8a6f60..be8299c1 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java @@ -49,9 +49,9 @@ public class IostorInvCreateReqVO { @Schema(description = "批次序列号") private String pcsn; - @Schema(description = "计划数量", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "计划数量不能为空") - @DecimalMin(value = "0.001", message = "计划数量不能小于 0.001") + @Schema(description = "出库重量", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出库重量不能为空") + @DecimalMin(value = "0.001", message = "出库重量必须大于0") private BigDecimal planQty; @Schema(description = "数量单位标识") diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index b4e63bb7..f361757f 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -7,7 +7,11 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import java.time.LocalDateTime; +import java.util.Collections; + import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest @ActiveProfiles("local") @@ -17,9 +21,17 @@ class IostorInvServiceLocalSpringTest { private IostorInvService iostorInvService; @Test - void createOutboundShouldRejectEmptyRequest() { + void createOutboundShouldRejectEmptyDetails() { IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO(); + reqVO.setBillType("OUTBOUND"); + reqVO.setStorId("stor-1"); + reqVO.setBizDate(LocalDateTime.now()); + reqVO.setDetails(Collections.emptyList()); - assertThrows(ConstraintViolationException.class, () -> iostorInvService.createOutbound(reqVO)); + ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, + () -> iostorInvService.createOutbound(reqVO)); + + assertTrue(exception.getConstraintViolations().stream() + .anyMatch(violation -> violation.getPropertyPath().toString().contains("details"))); } } From 1fcc152dcab6f06c5e24108e338da826cdac2d4f Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:36:21 +0800 Subject: [PATCH 06/24] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=8C=89?= =?UTF-8?q?=E7=AE=B1=E5=8F=B7=E5=B1=95=E5=BC=80=E5=8F=AF=E7=94=A8=E5=BA=93?= =?UTF-8?q?=E5=AD=98=E5=AD=90=E5=8D=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/iostorinv/IostorInvController.java | 16 +++++ .../vo/AvailableInventoryPageReqVO.java | 27 ++++++++ .../vo/AvailableInventoryRespVO.java | 41 ++++++++++++ .../vo/ExpandAvailableInventoryReqVO.java | 21 ++++++ .../iostorinvdtl/IostorinvDtlMapper.java | 28 +++++++- .../service/iostorinv/IostorInvService.java | 16 +++++ .../iostorinv/IostorInvServiceImpl.java | 16 +++++ .../iostorinvdtl/IostorinvDtlMapper.xml | 65 +++++++++++++++++-- .../IostorInvServiceLocalSpringTest.java | 24 +++++++ 9 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryPageReqVO.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java 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 8f3cadd8..919f9a4d 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 @@ -38,6 +38,22 @@ public class IostorInvController { @Resource private IostorInvService iostorInvService; + @GetMapping("/availableInventoryPage") + @Operation(summary = "获得可用库存分页") + @PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')") + public CommonResult> getAvailableInventoryPage( + @Valid AvailableInventoryPageReqVO reqVO) { + return success(iostorInvService.getAvailableInventoryPage(reqVO)); + } + + @PostMapping("/expandAvailableInventory") + @Operation(summary = "按箱号展开全部可用子卷") + @PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')") + public CommonResult> expandAvailableInventory( + @Valid @RequestBody ExpandAvailableInventoryReqVO reqVO) { + return success(iostorInvService.expandAvailableInventory(reqVO)); + } + @PostMapping("/create") @Operation(summary = "创建出入库单主表") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryPageReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryPageReqVO.java new file mode 100644 index 00000000..afc4f5fe --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryPageReqVO.java @@ -0,0 +1,27 @@ +package cn.code.nl.module.wms.controller.admin.iostorinv.vo; + +import cn.code.nl.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 可用库存分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class AvailableInventoryPageReqVO extends PageParam { + + @Schema(description = "仓库标识", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "仓库标识不能为空") + private String storId; + + @Schema(description = "物料编码") + private String materialCode; + + @Schema(description = "箱号") + private String vehicleCode; + + @Schema(description = "批次") + private String pcsn; + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java new file mode 100644 index 00000000..b6ba82e4 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java @@ -0,0 +1,41 @@ +package cn.code.nl.module.wms.controller.admin.iostorinv.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 可用库存 Response VO") +@Data +public class AvailableInventoryRespVO { + + @Schema(description = "箱号") + private String vehicleCode; + @Schema(description = "批次") + private String pcsn; + @Schema(description = "物料标识") + private String materialId; + @Schema(description = "物料编码") + private String materialCode; + @Schema(description = "物料名称") + private String materialName; + @Schema(description = "可用数量") + private BigDecimal availableQty; + @Schema(description = "计量单位标识") + private String qtyUnitId; + @Schema(description = "计量单位名称") + private String qtyUnitName; + @Schema(description = "来源单据号") + private String extCode; + @Schema(description = "来源单据类型") + private String extType; + @Schema(description = "来源单据明细号") + private String extDtlCode; + @Schema(description = "仓库标识") + private String storId; + @Schema(description = "仓库编码") + private String storCode; + @Schema(description = "仓库名称") + private String storName; + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java new file mode 100644 index 00000000..7becb7cf --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java @@ -0,0 +1,21 @@ +package cn.code.nl.module.wms.controller.admin.iostorinv.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 按箱号展开可用库存 Request VO") +@Data +public class ExpandAvailableInventoryReqVO { + + @Schema(description = "仓库标识", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "仓库标识不能为空") + private String storId; + + @Schema(description = "箱号列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "箱号列表不能为空") + private List vehicleCodes; + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java index df00b8ea..e4bdde7d 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java @@ -3,10 +3,15 @@ package cn.code.nl.module.wms.dal.mysql.iostorinvdtl; import java.util.*; import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.iostorinvdtl.IostorinvDtlDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryPageReqVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; /** @@ -17,6 +22,27 @@ import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; @Mapper public interface IostorinvDtlMapper extends BaseMapperX { + /** + * 分页查询指定仓库的可用库存。 + */ + default PageResult selectAvailableInventoryPage(AvailableInventoryPageReqVO reqVO) { + Page page = selectAvailableInventoryPage( + MyBatisUtils.buildPage(reqVO), reqVO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + /** + * 执行可用库存分页 SQL。 + */ + Page selectAvailableInventoryPage( + Page page, @Param("reqVO") AvailableInventoryPageReqVO reqVO); + + /** + * 查询指定仓库、指定箱号下的全部可用子卷。 + */ + List selectAvailableInventoryByVehicleCodes( + @Param("storId") String storId, @Param("vehicleCodes") Collection vehicleCodes); + default PageResult selectPage(IostorinvDtlPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(IostorinvDtlDO::getIostorinvId, reqVO.getIostorinvId()) @@ -41,4 +67,4 @@ public interface IostorinvDtlMapper extends BaseMapperX { .orderByDesc(IostorinvDtlDO::getIostorinvdtlId)); } -} \ No newline at end of file +} 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 68b722bf..1b28f1e6 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 @@ -22,6 +22,22 @@ public interface IostorInvService { */ String createOutbound(@Valid IostorInvCreateReqVO reqVO); + /** + * 获得指定仓库的可用库存分页。 + * + * @param reqVO 分页查询条件 + * @return 可用库存分页 + */ + PageResult getAvailableInventoryPage(@Valid AvailableInventoryPageReqVO reqVO); + + /** + * 按箱号展开指定仓库中的全部可用子卷。 + * + * @param reqVO 展开条件 + * @return 可用子卷列表 + */ + List expandAvailableInventory(@Valid ExpandAvailableInventoryReqVO 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 bcb984be..17623860 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 @@ -14,6 +14,7 @@ import cn.code.nl.framework.common.pojo.PageParam; import cn.code.nl.framework.common.util.object.BeanUtils; import cn.code.nl.module.wms.dal.mysql.iostorinv.IostorInvMapper; +import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper; import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.code.nl.framework.common.util.collection.CollectionUtils.convertList; @@ -32,11 +33,26 @@ public class IostorInvServiceImpl implements IostorInvService { @Resource private IostorInvMapper iostorInvMapper; + @Resource + private IostorinvDtlMapper iostorinvDtlMapper; + @Override public String createOutbound(IostorInvCreateReqVO reqVO) { throw new UnsupportedOperationException("出库单聚合保存功能尚未实现"); } + @Override + public PageResult getAvailableInventoryPage(AvailableInventoryPageReqVO reqVO) { + return iostorinvDtlMapper.selectAvailableInventoryPage(reqVO); + } + + @Override + public List expandAvailableInventory(ExpandAvailableInventoryReqVO reqVO) { + // 用户可以独立勾选子卷;按涉及箱号去重后,由数据库展开箱内全部可用子卷。 + Set vehicleCodes = new LinkedHashSet<>(reqVO.getVehicleCodes()); + return iostorinvDtlMapper.selectAvailableInventoryByVehicleCodes(reqVO.getStorId(), vehicleCodes); + } + @Override public String createIostorInv(IostorInvSaveReqVO createReqVO) { // 插入 diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml index 9e1b610f..0711072a 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml @@ -2,11 +2,62 @@ - + + gp.vehicle_code AS vehicle_code, + gp.pcsn AS pcsn, + gp.material_id AS material_id, + gp.material_code AS material_code, + mb.material_name AS material_name, + gp.qty - gp.frozen_qty AS available_qty, + gp.qty_unit_id AS qty_unit_id, + gp.qty_unit_name AS qty_unit_name, + gp.ext_code AS ext_code, + gp.ext_type AS ext_type, + gp.ext_dtl_code AS ext_dtl_code, + sa.stor_id AS stor_id, + sa.stor_code AS stor_code, + sa.stor_name AS stor_name + - \ No newline at end of file + + SELECT + FROM wms_group_plate gp + INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code + LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0 + WHERE sa.stor_id = #{reqVO.storId} + AND gp.status = '可用' + AND gp.qty - gp.frozen_qty > 0 + AND gp.is_deleted = 0 + AND sa.is_deleted = 0 + + AND gp.material_code LIKE CONCAT('%', #{reqVO.materialCode}, '%') + + + AND gp.vehicle_code LIKE CONCAT('%', #{reqVO.vehicleCode}, '%') + + + AND gp.pcsn LIKE CONCAT('%', #{reqVO.pcsn}, '%') + + ORDER BY gp.vehicle_code, gp.group_id + + + + SELECT + FROM wms_group_plate gp + INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code + LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0 + WHERE sa.stor_id = #{storId} + AND gp.status = '可用' + AND gp.qty - gp.frozen_qty > 0 + AND gp.is_deleted = 0 + AND sa.is_deleted = 0 + AND gp.vehicle_code IN + + #{vehicleCode} + + ORDER BY gp.vehicle_code, gp.group_id + + + diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index f361757f..438a6295 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -1,17 +1,22 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.Collections; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest @ActiveProfiles("local") @@ -34,4 +39,23 @@ class IostorInvServiceLocalSpringTest { assertTrue(exception.getConstraintViolations().stream() .anyMatch(violation -> violation.getPropertyPath().toString().contains("details"))); } + + /** + * 本地库数据前置:仓库 stor-1 的 BOX-001 有两个可用子卷,另一仓库也存在 BOX-001。 + * 当前仓库没有可移植的 WMS 本地测试数据集,避免向开发库写入固定业务数据,准备好上述数据后可手动启用。 + */ + @Test + @Disabled("需要 local 数据库预置跨仓库 BOX-001 测试数据") + void expandAvailableInventoryShouldReturnAllChildrenOnceAndOnlyFromRequestedWarehouse() { + ExpandAvailableInventoryReqVO reqVO = new ExpandAvailableInventoryReqVO(); + reqVO.setStorId("stor-1"); + reqVO.setVehicleCodes(List.of("BOX-001", "BOX-001")); + + List result = iostorInvService.expandAvailableInventory(reqVO); + + assertEquals(2, result.size()); + assertTrue(result.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); + assertTrue(result.stream().allMatch(item -> "stor-1".equals(item.getStorId()))); + assertEquals(2, result.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); + } } From 3d180b49481c710119d7c2e375896698b5f0c478 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:39:49 +0800 Subject: [PATCH 07/24] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E5=BA=93=E5=AD=98=E8=BF=87=E6=BB=A4=E4=B8=8E=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iostorinvdtl/IostorinvDtlMapper.xml | 12 +-- .../iostorinv/IostorInvServiceImplTest.java | 78 +++++++++++++++++++ .../IostorInvServiceLocalSpringTest.java | 23 ------ 3 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml index 0711072a..0b15695c 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml @@ -24,12 +24,12 @@ SELECT FROM wms_group_plate gp INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code - LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0 + LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 WHERE sa.stor_id = #{reqVO.storId} AND gp.status = '可用' AND gp.qty - gp.frozen_qty > 0 - AND gp.is_deleted = 0 - AND sa.is_deleted = 0 + AND gp.deleted = 0 + AND sa.deleted = 0 AND gp.material_code LIKE CONCAT('%', #{reqVO.materialCode}, '%') @@ -47,12 +47,12 @@ SELECT FROM wms_group_plate gp INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code - LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0 + LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 WHERE sa.stor_id = #{storId} AND gp.status = '可用' AND gp.qty - gp.frozen_qty > 0 - AND gp.is_deleted = 0 - AND sa.is_deleted = 0 + AND gp.deleted = 0 + AND sa.deleted = 0 AND gp.vehicle_code IN #{vehicleCode} diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java new file mode 100644 index 00000000..c2c9c1f6 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java @@ -0,0 +1,78 @@ +package cn.code.nl.module.wms.service.iostorinv; + +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO; +import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IostorInvServiceImplTest { + + @Test + @SuppressWarnings("unchecked") + void expandAvailableInventoryShouldDeduplicateBoxesAndKeepWarehouseIsolation() throws Exception { + List queriedVehicleCodes = new ArrayList<>(); + IostorinvDtlMapper mapper = (IostorinvDtlMapper) Proxy.newProxyInstance( + IostorinvDtlMapper.class.getClassLoader(), new Class>[]{IostorinvDtlMapper.class}, + (proxy, method, args) -> { + if (!"selectAvailableInventoryByVehicleCodes".equals(method.getName())) { + throw new UnsupportedOperationException(method.getName()); + } + assertEquals("STOR-001", args[0]); + queriedVehicleCodes.addAll((Collection) args[1]); + return List.of(inventory("BOX-001", "PCSN-001", "STOR-001"), + inventory("BOX-001", "PCSN-002", "STOR-001")); + }); + IostorInvServiceImpl service = new IostorInvServiceImpl(); + Field mapperField = IostorInvServiceImpl.class.getDeclaredField("iostorinvDtlMapper"); + mapperField.setAccessible(true); + mapperField.set(service, mapper); + + ExpandAvailableInventoryReqVO reqVO = new ExpandAvailableInventoryReqVO(); + reqVO.setStorId("STOR-001"); + reqVO.setVehicleCodes(List.of("BOX-001", "BOX-001")); + + List result = service.expandAvailableInventory(reqVO); + + assertEquals(List.of("BOX-001"), queriedVehicleCodes); + assertEquals(2, result.size()); + assertTrue(result.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); + assertTrue(result.stream().allMatch(item -> "STOR-001".equals(item.getStorId()))); + assertEquals(2, result.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); + } + + @Test + void availableInventorySqlShouldFilterWarehouseStatusDeletedAndAvailableQuantity() throws Exception { + String mapperResource = "mapper/iostorinvdtl/IostorinvDtlMapper.xml"; + try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) { + assertNotNull(input, "Mapper XML 应存在于测试类路径"); + String sql = new String(input.readAllBytes(), StandardCharsets.UTF_8); + assertTrue(sql.contains("sa.stor_id = #{storId}")); + assertTrue(sql.contains("gp.status = '可用'")); + assertTrue(sql.contains("gp.qty - gp.frozen_qty > 0")); + assertTrue(sql.contains("gp.deleted = 0")); + assertTrue(sql.contains("sa.deleted = 0")); + assertTrue(sql.contains("mb.deleted = 0")); + } + } + + private static AvailableInventoryRespVO inventory(String vehicleCode, String pcsn, String storId) { + AvailableInventoryRespVO inventory = new AvailableInventoryRespVO(); + inventory.setVehicleCode(vehicleCode); + inventory.setPcsn(pcsn); + inventory.setStorId(storId); + return inventory; + } + +} diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index 438a6295..b6615b70 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -1,22 +1,17 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; -import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; -import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Disabled; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.Collections; -import java.util.List; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest @ActiveProfiles("local") @@ -40,22 +35,4 @@ class IostorInvServiceLocalSpringTest { .anyMatch(violation -> violation.getPropertyPath().toString().contains("details"))); } - /** - * 本地库数据前置:仓库 stor-1 的 BOX-001 有两个可用子卷,另一仓库也存在 BOX-001。 - * 当前仓库没有可移植的 WMS 本地测试数据集,避免向开发库写入固定业务数据,准备好上述数据后可手动启用。 - */ - @Test - @Disabled("需要 local 数据库预置跨仓库 BOX-001 测试数据") - void expandAvailableInventoryShouldReturnAllChildrenOnceAndOnlyFromRequestedWarehouse() { - ExpandAvailableInventoryReqVO reqVO = new ExpandAvailableInventoryReqVO(); - reqVO.setStorId("stor-1"); - reqVO.setVehicleCodes(List.of("BOX-001", "BOX-001")); - - List result = iostorInvService.expandAvailableInventory(reqVO); - - assertEquals(2, result.size()); - assertTrue(result.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); - assertTrue(result.stream().allMatch(item -> "stor-1".equals(item.getStorId()))); - assertEquals(2, result.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); - } } From 2e87bf70aafd5e2a1a1192ee7918f8dfb2168dfb Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:42:58 +0800 Subject: [PATCH 08/24] =?UTF-8?q?test:=20=E4=BD=BF=E7=94=A8=20H2=20?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=8F=AF=E7=94=A8=E5=BA=93=E5=AD=98=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nl-module-wms/nl-module-wms-server/pom.xml | 12 ++ .../iostorinvdtl/IostorinvDtlMapperTest.java | 111 ++++++++++++++++++ .../iostorinv/IostorInvServiceImplTest.java | 78 ------------ .../sql/iostorinv/available-inventory.sql | 56 +++++++++ 4 files changed, 179 insertions(+), 78 deletions(-) create mode 100644 nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java delete mode 100644 nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java create mode 100644 nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql diff --git a/nl-module-wms/nl-module-wms-server/pom.xml b/nl-module-wms/nl-module-wms-server/pom.xml index 4142363f..26dc0cdf 100644 --- a/nl-module-wms/nl-module-wms-server/pom.xml +++ b/nl-module-wms/nl-module-wms-server/pom.xml @@ -129,6 +129,18 @@ cn.nl.cloud nl-spring-boot-starter-monitor + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + test + diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java new file mode 100644 index 00000000..8b673002 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java @@ -0,0 +1,111 @@ +package cn.code.nl.module.wms.dal.mysql.iostorinvdtl; + +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryPageReqVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO; +import cn.code.nl.module.wms.service.iostorinv.IostorInvServiceImpl; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import org.apache.ibatis.session.SqlSessionFactory; +import org.junit.jupiter.api.Test; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +import javax.sql.DataSource; +import jakarta.annotation.Resource; +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringJUnitConfig +@ContextConfiguration(classes = IostorinvDtlMapperTest.TestConfiguration.class) +@Sql("/sql/iostorinv/available-inventory.sql") +@Transactional +class IostorinvDtlMapperTest { + + @Resource + private IostorinvDtlMapper mapper; + + @Test + void shouldExpandAvailableChildrenAndExecutePageFilters() throws Exception { + IostorInvServiceImpl service = new IostorInvServiceImpl(); + Field mapperField = IostorInvServiceImpl.class.getDeclaredField("iostorinvDtlMapper"); + mapperField.setAccessible(true); + mapperField.set(service, mapper); + + ExpandAvailableInventoryReqVO expandReqVO = new ExpandAvailableInventoryReqVO(); + expandReqVO.setStorId("STOR-001"); + expandReqVO.setVehicleCodes(List.of("BOX-001", "BOX-001")); + List expanded = service.expandAvailableInventory(expandReqVO); + + assertEquals(2, expanded.size()); + assertTrue(expanded.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); + assertTrue(expanded.stream().allMatch(item -> "STOR-001".equals(item.getStorId()))); + assertEquals(2, expanded.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); + assertTrue(expanded.stream().allMatch(item -> item.getAvailableQty().signum() > 0)); + + AvailableInventoryPageReqVO warehousePageReqVO = new AvailableInventoryPageReqVO(); + warehousePageReqVO.setStorId("STOR-001"); + warehousePageReqVO.setPageNo(1); + warehousePageReqVO.setPageSize(10); + PageResult warehousePage = mapper.selectAvailableInventoryPage(warehousePageReqVO); + assertEquals(2L, warehousePage.getTotal()); + + AvailableInventoryPageReqVO pageReqVO = new AvailableInventoryPageReqVO(); + pageReqVO.setStorId("STOR-001"); + pageReqVO.setMaterialCode("MAT-001"); + pageReqVO.setVehicleCode("BOX-001"); + pageReqVO.setPcsn("PCSN-002"); + pageReqVO.setPageNo(1); + pageReqVO.setPageSize(10); + PageResult page = mapper.selectAvailableInventoryPage(pageReqVO); + + assertEquals(1L, page.getTotal()); + assertEquals("PCSN-002", page.getList().get(0).getPcsn()); + assertEquals("材料一", page.getList().get(0).getMaterialName()); + } + + @Configuration(proxyBeanMethods = false) + @MapperScan(basePackageClasses = IostorinvDtlMapper.class) + static class TestConfiguration { + + @Bean + DataSource dataSource() { + return new DriverManagerDataSource("jdbc:h2:mem:available_inventory;MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + } + + @Bean + SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(dataSource); + factory.setMapperLocations(new org.springframework.core.io.ClassPathResource( + "mapper/iostorinvdtl/IostorinvDtlMapper.xml")); + org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); + factory.setConfiguration(configuration); + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); + factory.setPlugins(interceptor); + return factory.getObject(); + } + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + } + +} diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java deleted file mode 100644 index c2c9c1f6..00000000 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImplTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package cn.code.nl.module.wms.service.iostorinv; - -import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; -import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO; -import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper; -import org.junit.jupiter.api.Test; - -import java.io.InputStream; -import java.lang.reflect.Field; -import java.lang.reflect.Proxy; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class IostorInvServiceImplTest { - - @Test - @SuppressWarnings("unchecked") - void expandAvailableInventoryShouldDeduplicateBoxesAndKeepWarehouseIsolation() throws Exception { - List queriedVehicleCodes = new ArrayList<>(); - IostorinvDtlMapper mapper = (IostorinvDtlMapper) Proxy.newProxyInstance( - IostorinvDtlMapper.class.getClassLoader(), new Class>[]{IostorinvDtlMapper.class}, - (proxy, method, args) -> { - if (!"selectAvailableInventoryByVehicleCodes".equals(method.getName())) { - throw new UnsupportedOperationException(method.getName()); - } - assertEquals("STOR-001", args[0]); - queriedVehicleCodes.addAll((Collection) args[1]); - return List.of(inventory("BOX-001", "PCSN-001", "STOR-001"), - inventory("BOX-001", "PCSN-002", "STOR-001")); - }); - IostorInvServiceImpl service = new IostorInvServiceImpl(); - Field mapperField = IostorInvServiceImpl.class.getDeclaredField("iostorinvDtlMapper"); - mapperField.setAccessible(true); - mapperField.set(service, mapper); - - ExpandAvailableInventoryReqVO reqVO = new ExpandAvailableInventoryReqVO(); - reqVO.setStorId("STOR-001"); - reqVO.setVehicleCodes(List.of("BOX-001", "BOX-001")); - - List result = service.expandAvailableInventory(reqVO); - - assertEquals(List.of("BOX-001"), queriedVehicleCodes); - assertEquals(2, result.size()); - assertTrue(result.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); - assertTrue(result.stream().allMatch(item -> "STOR-001".equals(item.getStorId()))); - assertEquals(2, result.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); - } - - @Test - void availableInventorySqlShouldFilterWarehouseStatusDeletedAndAvailableQuantity() throws Exception { - String mapperResource = "mapper/iostorinvdtl/IostorinvDtlMapper.xml"; - try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) { - assertNotNull(input, "Mapper XML 应存在于测试类路径"); - String sql = new String(input.readAllBytes(), StandardCharsets.UTF_8); - assertTrue(sql.contains("sa.stor_id = #{storId}")); - assertTrue(sql.contains("gp.status = '可用'")); - assertTrue(sql.contains("gp.qty - gp.frozen_qty > 0")); - assertTrue(sql.contains("gp.deleted = 0")); - assertTrue(sql.contains("sa.deleted = 0")); - assertTrue(sql.contains("mb.deleted = 0")); - } - } - - private static AvailableInventoryRespVO inventory(String vehicleCode, String pcsn, String storId) { - AvailableInventoryRespVO inventory = new AvailableInventoryRespVO(); - inventory.setVehicleCode(vehicleCode); - inventory.setPcsn(pcsn); - inventory.setStorId(storId); - return inventory; - } - -} diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql new file mode 100644 index 00000000..8d08b35b --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql @@ -0,0 +1,56 @@ +DROP TABLE IF EXISTS wms_group_plate; +DROP TABLE IF EXISTS wms_structattr; +DROP TABLE IF EXISTS base_materialbase; + +CREATE TABLE base_materialbase ( + material_id BIGINT PRIMARY KEY, + material_name VARCHAR(64), + deleted BOOLEAN NOT NULL +); + +CREATE TABLE wms_structattr ( + struct_id VARCHAR(64) PRIMARY KEY, + storagevehicle_code VARCHAR(64), + stor_id VARCHAR(64), + stor_code VARCHAR(64), + stor_name VARCHAR(64), + deleted BOOLEAN NOT NULL +); + +CREATE TABLE wms_group_plate ( + group_id BIGINT PRIMARY KEY, + vehicle_code VARCHAR(64), + status VARCHAR(16), + material_id VARCHAR(64), + material_code VARCHAR(64), + pcsn VARCHAR(64), + qty DECIMAL(18, 6), + frozen_qty DECIMAL(18, 6), + qty_unit_id VARCHAR(64), + qty_unit_name VARCHAR(64), + ext_code VARCHAR(64), + ext_type VARCHAR(64), + ext_dtl_code VARCHAR(64), + deleted BOOLEAN NOT NULL +); + +INSERT INTO base_materialbase(material_id, material_name, deleted) +VALUES (1001, '材料一', FALSE), (1002, '已删除材料', TRUE); + +INSERT INTO wms_structattr(struct_id, storagevehicle_code, stor_id, stor_code, stor_name, deleted) +VALUES ('S-001', 'BOX-001', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), + ('S-002', 'BOX-001', 'STOR-002', 'STOR-CODE-002', '二号仓', FALSE), + ('S-003', 'BOX-002', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), + ('S-004', 'BOX-003', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), + ('S-005', 'BOX-004', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), + ('S-006', 'BOX-005', 'STOR-001', 'STOR-CODE-001', '一号仓', TRUE); + +INSERT INTO wms_group_plate(group_id, vehicle_code, status, material_id, material_code, pcsn, + qty, frozen_qty, qty_unit_id, qty_unit_name, ext_code, ext_type, + ext_dtl_code, deleted) +VALUES (1, 'BOX-001', '可用', '1001', 'MAT-001', 'PCSN-001', 10, 2, 'UNIT-1', '卷', 'EXT-1', 'TYPE-1', 'DTL-1', FALSE), + (2, 'BOX-001', '可用', '1001', 'MAT-001', 'PCSN-002', 5, 1, 'UNIT-1', '卷', 'EXT-2', 'TYPE-1', 'DTL-2', FALSE), + (3, 'BOX-002', '不可用', '1001', 'MAT-001', 'PCSN-003', 10, 0, 'UNIT-1', '卷', 'EXT-3', 'TYPE-1', 'DTL-3', FALSE), + (4, 'BOX-003', '可用', '1001', 'MAT-001', 'PCSN-004', 10, 10, 'UNIT-1', '卷', 'EXT-4', 'TYPE-1', 'DTL-4', FALSE), + (5, 'BOX-004', '可用', '1001', 'MAT-001', 'PCSN-005', 10, 0, 'UNIT-1', '卷', 'EXT-5', 'TYPE-1', 'DTL-5', TRUE), + (6, 'BOX-005', '可用', '1001', 'MAT-001', 'PCSN-006', 10, 0, 'UNIT-1', '卷', 'EXT-6', 'TYPE-1', 'DTL-6', FALSE); From 7105731d9d8aa3b7a30b72e8383890e00d478ad6 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:47:46 +0800 Subject: [PATCH 09/24] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E5=BA=93=E5=AD=98=E5=85=B3=E8=81=94=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/ExpandAvailableInventoryReqVO.java | 5 ++++- .../mapper/iostorinvdtl/IostorinvDtlMapper.xml | 16 ++++++++++++---- .../sql/iostorinv/available-inventory.sql | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java index 7becb7cf..f35adf56 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java @@ -1,7 +1,9 @@ package cn.code.nl.module.wms.controller.admin.iostorinv.vo; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; import lombok.Data; import java.util.List; @@ -16,6 +18,7 @@ public class ExpandAvailableInventoryReqVO { @Schema(description = "箱号列表", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "箱号列表不能为空") - private List vehicleCodes; + @Size(max = 500, message = "箱号数量不能超过 500 个") + private List<@NotBlank(message = "箱号不能为空") String> vehicleCodes; } diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml index 0b15695c..6b2947f3 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml @@ -23,13 +23,17 @@ resultType="cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO"> SELECT FROM wms_group_plate gp - INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code + INNER JOIN ( + SELECT stor_id, storagevehicle_code, MAX(stor_code) AS stor_code, MAX(stor_name) AS stor_name + FROM wms_structattr + WHERE deleted = 0 + GROUP BY stor_id, storagevehicle_code + ) sa ON sa.storagevehicle_code = gp.vehicle_code LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 WHERE sa.stor_id = #{reqVO.storId} AND gp.status = '可用' AND gp.qty - gp.frozen_qty > 0 AND gp.deleted = 0 - AND sa.deleted = 0 AND gp.material_code LIKE CONCAT('%', #{reqVO.materialCode}, '%') @@ -46,13 +50,17 @@ resultType="cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO"> SELECT FROM wms_group_plate gp - INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code + INNER JOIN ( + SELECT stor_id, storagevehicle_code, MAX(stor_code) AS stor_code, MAX(stor_name) AS stor_name + FROM wms_structattr + WHERE deleted = 0 + GROUP BY stor_id, storagevehicle_code + ) sa ON sa.storagevehicle_code = gp.vehicle_code LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 WHERE sa.stor_id = #{storId} AND gp.status = '可用' AND gp.qty - gp.frozen_qty > 0 AND gp.deleted = 0 - AND sa.deleted = 0 AND gp.vehicle_code IN #{vehicleCode} diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql index 8d08b35b..3cd89ffc 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql @@ -39,6 +39,7 @@ VALUES (1001, '材料一', FALSE), (1002, '已删除材料', TRUE); INSERT INTO wms_structattr(struct_id, storagevehicle_code, stor_id, stor_code, stor_name, deleted) VALUES ('S-001', 'BOX-001', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), + ('S-001-B', 'BOX-001', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), ('S-002', 'BOX-001', 'STOR-002', 'STOR-CODE-002', '二号仓', FALSE), ('S-003', 'BOX-002', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), ('S-004', 'BOX-003', 'STOR-001', 'STOR-CODE-001', '一号仓', FALSE), From e562715dd89d52a0095a9999cfd6a7276466792e Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:52:51 +0800 Subject: [PATCH 10/24] =?UTF-8?q?feat:=20=E4=BA=8B=E5=8A=A1=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=87=BA=E5=BA=93=E5=8D=95=E5=8F=8A=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/wms/enums/ErrorCodeConstants.java | 2 + .../admin/iostorinv/IostorInvController.java | 7 + .../iostorinv/IostorInvServiceImpl.java | 59 ++++- .../IostorInvServiceLocalSpringTest.java | 202 ++++++++++++++++-- .../sql/iostorinv/create-outbound.sql | 30 +++ 5 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql 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 6205362e..955f2fdb 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 @@ -12,6 +12,8 @@ public interface ErrorCodeConstants { // ================ 出入库表相关错误码 ================= ErrorCode IOSTOR_INV_NOT_EXISTS = new ErrorCode(4, "出入库单主表不存在"); + ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(7, "单据号生成失败"); + ErrorCode IOSTORINV_DTL_NOT_EXISTS = new ErrorCode(5, "出入库单明细不存在"); ErrorCode IOSTORINV_DIS_NOT_EXISTS = new ErrorCode(6, "出入库单分配不存在"); 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 919f9a4d..82f8c078 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 @@ -38,6 +38,13 @@ public class IostorInvController { @Resource private IostorInvService iostorInvService; + @PostMapping("/createOutbound") + @Operation(summary = "创建出库单及明细") + @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") + public CommonResult createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) { + return success(iostorInvService.createOutbound(reqVO)); + } + @GetMapping("/availableInventoryPage") @Operation(summary = "获得可用库存分页") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')") 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 17623860..89623dbf 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 @@ -1,11 +1,17 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.hutool.core.collection.CollUtil; +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.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.util.*; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*; import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO; @@ -28,6 +34,7 @@ import static cn.code.nl.module.wms.enums.ErrorCodeConstants.*; */ @Service @Validated +@Slf4j public class IostorInvServiceImpl implements IostorInvService { @Resource @@ -36,9 +43,59 @@ public class IostorInvServiceImpl implements IostorInvService { @Resource private IostorinvDtlMapper iostorinvDtlMapper; + @Resource + private CodeGenApi codeGenApi; + + /** + * 创建出库单及其明细。 + */ @Override + @Transactional(rollbackFor = Exception.class) public String createOutbound(IostorInvCreateReqVO reqVO) { - throw new UnsupportedOperationException("出库单聚合保存功能尚未实现"); + CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO(); + codeGenerateReqDTO.setRuleCode("IO_CODE"); + CommonResult codeResult = codeGenApi.generate(codeGenerateReqDTO); + if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) { + String remoteMessage = codeResult == null ? "返回结果为空" : codeResult.getMsg(); + log.error("[createOutbound][生成出库单号失败,远程信息:{}]", remoteMessage); + throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); + } + + BigDecimal totalWeight = reqVO.getDetails().stream() + .map(IostorInvCreateReqVO.Detail::getPlanQty) + .reduce(BigDecimal.ZERO, BigDecimal::add); + IostorInvDO iostorInv = new IostorInvDO(); + iostorInv.setBillCode(codeResult.getData()); + iostorInv.setIoType("OUT"); + iostorInv.setBillType(reqVO.getBillType()); + iostorInv.setBizDate(reqVO.getBizDate()); + iostorInv.setStorId(reqVO.getStorId()); + iostorInv.setBillStatus("生成"); + iostorInv.setRemark(reqVO.getRemark()); + iostorInv.setDetailCount(reqVO.getDetails().size()); + iostorInv.setTotalWeight(totalWeight); + iostorInvMapper.insert(iostorInv); + + for (int index = 0; index < reqVO.getDetails().size(); index++) { + IostorInvCreateReqVO.Detail detail = reqVO.getDetails().get(index); + IostorinvDtlDO detailDO = new IostorinvDtlDO(); + detailDO.setIostorinvId(iostorInv.getIostorinvId()); + detailDO.setSeqNo(index + 1); + detailDO.setMaterialCode(detail.getMaterialCode()); + detailDO.setMaterialId(detail.getMaterialId()); + detailDO.setPcsn(detail.getPcsn()); + detailDO.setPlanQty(detail.getPlanQty()); + detailDO.setUnassignQty(detail.getPlanQty()); + detailDO.setAssignQty(BigDecimal.ZERO); + detailDO.setQtyUnitId(detail.getQtyUnitId()); + detailDO.setQtyUnitName(detail.getQtyUnitName()); + detailDO.setSourceBillCode(detail.getSourceBillCode()); + detailDO.setSourceBillType(detail.getSourceBillType()); + detailDO.setSourceBilldtlId(detail.getSourceBilldtlId()); + detailDO.setRemark(detail.getRemark()); + iostorinvDtlMapper.insert(detailDO); + } + return iostorInv.getIostorinvId(); } @Override diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index b6615b70..0632d5b5 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -1,38 +1,200 @@ package cn.code.nl.module.wms.service.iostorinv; +import cn.code.nl.framework.common.exception.ServiceException; +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.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.incrementer.H2KeyGenerator; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import jakarta.annotation.Resource; -import jakarta.validation.ConstraintViolationException; +import org.apache.ibatis.session.SqlSessionFactory; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import javax.sql.DataSource; +import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.Collections; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -@SpringBootTest -@ActiveProfiles("local") +@SpringJUnitConfig +@ContextConfiguration(classes = IostorInvServiceLocalSpringTest.TestConfiguration.class) +@Sql("/sql/iostorinv/create-outbound.sql") class IostorInvServiceLocalSpringTest { @Resource private IostorInvService iostorInvService; + @Resource + private JdbcTemplate jdbcTemplate; + @Resource + private TestCodeGenApi codeGenApi; - @Test - void createOutboundShouldRejectEmptyDetails() { - IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO(); - reqVO.setBillType("OUTBOUND"); - reqVO.setStorId("stor-1"); - reqVO.setBizDate(LocalDateTime.now()); - reqVO.setDetails(Collections.emptyList()); - - ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, - () -> iostorInvService.createOutbound(reqVO)); - - assertTrue(exception.getConstraintViolations().stream() - .anyMatch(violation -> violation.getPropertyPath().toString().contains("details"))); + @BeforeEach + void resetCodeGenApi() { + codeGenApi.setResult(CommonResult.success("OUT-TEST-0001")); } + @Test + void shouldCreateOutboundHeaderAndDetailsInOneTransaction() { + String id = iostorInvService.createOutbound(buildRequest()); + + assertNotNull(id); + jdbcTemplate.queryForObject("SELECT * FROM wms_iostorinv WHERE iostorinv_id = ?", (rs, rowNum) -> { + assertEquals("OUT-TEST-0001", rs.getString("bill_code")); + assertEquals("OUT", rs.getString("io_type")); + assertEquals("销售出库", rs.getString("bill_type")); + assertEquals("STOR-01", rs.getString("stor_id")); + assertEquals("生成", rs.getString("bill_status")); + assertEquals("主表备注", rs.getString("remark")); + assertEquals(2, rs.getInt("detail_count")); + assertEquals(new BigDecimal("19.750"), rs.getBigDecimal("total_weight")); + return null; + }, id); + + List details = jdbcTemplate.query( + "SELECT * FROM wms_iostorinvdtl WHERE iostorinv_id = ? ORDER BY seq_no", + (rs, rowNum) -> new DetailRow(rs.getInt("seq_no"), rs.getString("material_code"), + rs.getString("material_id"), rs.getString("pcsn"), rs.getBigDecimal("plan_qty"), + rs.getBigDecimal("assign_qty"), rs.getBigDecimal("unassign_qty"), + rs.getString("qty_unit_id"), rs.getString("qty_unit_name"), + rs.getString("source_bill_code"), rs.getString("source_bill_type"), + rs.getString("source_billdtl_id"), rs.getString("remark")), id); + assertEquals(List.of( + new DetailRow(1, "MAT-01", "MID-01", "PCSN-01", new BigDecimal("12.500"), + BigDecimal.ZERO.setScale(3), new BigDecimal("12.500"), "KG", "千克", + "SRC-01", "ORDER", "SRC-DTL-01", "明细一"), + new DetailRow(2, "MAT-02", "MID-02", "PCSN-02", new BigDecimal("7.250"), + BigDecimal.ZERO.setScale(3), new BigDecimal("7.250"), "KG", "千克", + "SRC-02", "ORDER", "SRC-DTL-02", "明细二")), details); + } + + @Test + void shouldNotInsertAnythingWhenCodeGenerationFails() { + codeGenApi.setResult(CommonResult.error(500, "编码服务暂不可用")); + + ServiceException exception = assertThrows(ServiceException.class, + () -> iostorInvService.createOutbound(buildRequest())); + + assertEquals("单据号生成失败", exception.getMessage()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + + private IostorInvCreateReqVO buildRequest() { + IostorInvCreateReqVO request = new IostorInvCreateReqVO(); + request.setBillType("销售出库"); + request.setStorId("STOR-01"); + request.setBizDate(LocalDateTime.of(2026, 7, 22, 0, 0)); + request.setRemark("主表备注"); + request.setDetails(List.of(buildDetail("01", "12.500", "明细一"), + buildDetail("02", "7.250", "明细二"))); + return request; + } + + private IostorInvCreateReqVO.Detail buildDetail(String suffix, String planQty, String remark) { + IostorInvCreateReqVO.Detail detail = new IostorInvCreateReqVO.Detail(); + detail.setMaterialCode("MAT-" + suffix); + detail.setMaterialId("MID-" + suffix); + detail.setPcsn("PCSN-" + suffix); + detail.setPlanQty(new BigDecimal(planQty)); + detail.setQtyUnitId("KG"); + detail.setQtyUnitName("千克"); + detail.setSourceBillCode("SRC-" + suffix); + detail.setSourceBillType("ORDER"); + detail.setSourceBilldtlId("SRC-DTL-" + suffix); + detail.setRemark(remark); + return detail; + } + + private record DetailRow(Integer seqNo, String materialCode, String materialId, String pcsn, + BigDecimal planQty, BigDecimal assignQty, BigDecimal unassignQty, + String qtyUnitId, String qtyUnitName, String sourceBillCode, + String sourceBillType, String sourceBilldtlId, String remark) { + } + + @Configuration(proxyBeanMethods = false) + @EnableTransactionManagement + @MapperScan(basePackages = {"cn.code.nl.module.wms.dal.mysql.iostorinv", + "cn.code.nl.module.wms.dal.mysql.iostorinvdtl"}) + static class TestConfiguration { + + @Bean + DataSource dataSource() { + return new DriverManagerDataSource("jdbc:h2:mem:create_outbound;MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + } + + @Bean + SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(dataSource); + GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig(); + dbConfig.setKeyGenerators(List.of(new H2KeyGenerator())); + factory.setGlobalConfig(new GlobalConfig().setDbConfig(dbConfig)); + org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); + factory.setConfiguration(configuration); + return factory.getObject(); + } + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean + JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + static MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } + + @Bean + IostorInvService iostorInvService() { + return new IostorInvServiceImpl(); + } + + @Bean + TestCodeGenApi codeGenApi() { + return new TestCodeGenApi(); + } + } + + static class TestCodeGenApi implements CodeGenApi { + private CommonResult result; + + void setResult(CommonResult result) { + this.result = result; + } + + @Override + public CommonResult generate(CodeGenerateReqDTO reqDTO) { + assertEquals("IO_CODE", reqDTO.getRuleCode()); + return result; + } + + @Override + public CommonResult preview(String ruleCode) { + throw new UnsupportedOperationException(); + } + } } diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql new file mode 100644 index 00000000..8d3f1e65 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -0,0 +1,30 @@ +DROP TABLE IF EXISTS wms_iostorinvdtl; +DROP TABLE IF EXISTS wms_iostorinv; +DROP SEQUENCE IF EXISTS wms_iostorinvdtl_seq; +DROP SEQUENCE IF EXISTS wms_iostorinv_seq; + +CREATE SEQUENCE wms_iostorinv_seq START WITH 1; +CREATE SEQUENCE wms_iostorinvdtl_seq START WITH 1; + +CREATE TABLE wms_iostorinv ( + iostorinv_id VARCHAR(64) PRIMARY KEY, + bill_code VARCHAR(64), io_type VARCHAR(32), bill_type VARCHAR(64), biz_date TIMESTAMP, + stor_id VARCHAR(64), stor_code VARCHAR(64), stor_name VARCHAR(128), source_id VARCHAR(64), + source_name VARCHAR(128), source_type VARCHAR(64), total_qty DECIMAL(18, 3), + total_weight DECIMAL(18, 3), detail_count INT, bill_status VARCHAR(32), remark VARCHAR(255), + create_mode VARCHAR(32), dis_optid VARCHAR(64), dis_time TIMESTAMP, confirm_optid VARCHAR(64), + confirm_time TIMESTAMP, sysdeptid VARCHAR(64), syscompanyid VARCHAR(64), is_upload VARCHAR(16), + upload_optid VARCHAR(64), upload_time VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, + creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE +); + +CREATE TABLE wms_iostorinvdtl ( + iostorinvdtl_id VARCHAR(64) PRIMARY KEY, iostorinv_id VARCHAR(64), seq_no INT, + pcsn VARCHAR(128), bill_status VARCHAR(32), qty_unit_id VARCHAR(64), qty_unit_name VARCHAR(64), + plan_qty DECIMAL(18, 3), real_qty DECIMAL(18, 3), source_billdtl_id VARCHAR(64), + source_bill_type VARCHAR(64), source_bill_code VARCHAR(64), source_bill_table VARCHAR(128), + remark VARCHAR(255), assign_qty DECIMAL(18, 3), unassign_qty DECIMAL(18, 3), + material_code VARCHAR(64), source_load_port VARCHAR(64), callback_strategy VARCHAR(255), + material_id VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, + creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE +); From 3ab5af0eb538b078b82542dbde59e9cb55713c4c Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 14:55:49 +0800 Subject: [PATCH 11/24] =?UTF-8?q?fix:=20=E6=98=BE=E5=BC=8F=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=87=BA=E5=BA=93=E5=8D=95=E8=81=9A=E5=90=88=E4=B8=BB?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iostorinv/IostorInvServiceImpl.java | 3 ++ .../IostorInvServiceLocalSpringTest.java | 48 +++++++++++++------ .../sql/iostorinv/create-outbound.sql | 8 +--- 3 files changed, 38 insertions(+), 21 deletions(-) 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 89623dbf..ffccd527 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 @@ -1,6 +1,7 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; 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; @@ -65,6 +66,7 @@ public class IostorInvServiceImpl implements IostorInvService { .map(IostorInvCreateReqVO.Detail::getPlanQty) .reduce(BigDecimal.ZERO, BigDecimal::add); IostorInvDO iostorInv = new IostorInvDO(); + iostorInv.setIostorinvId(IdUtil.fastSimpleUUID()); iostorInv.setBillCode(codeResult.getData()); iostorInv.setIoType("OUT"); iostorInv.setBillType(reqVO.getBillType()); @@ -79,6 +81,7 @@ public class IostorInvServiceImpl implements IostorInvService { for (int index = 0; index < reqVO.getDetails().size(); index++) { IostorInvCreateReqVO.Detail detail = reqVO.getDetails().get(index); IostorinvDtlDO detailDO = new IostorinvDtlDO(); + detailDO.setIostorinvdtlId(IdUtil.fastSimpleUUID()); detailDO.setIostorinvId(iostorInv.getIostorinvId()); detailDO.setSeqNo(index + 1); detailDO.setMaterialCode(detail.getMaterialCode()); diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index 0632d5b5..5b3f3365 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -5,8 +5,6 @@ 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.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; -import com.baomidou.mybatisplus.core.config.GlobalConfig; -import com.baomidou.mybatisplus.extension.incrementer.H2KeyGenerator; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import jakarta.annotation.Resource; import org.apache.ibatis.session.SqlSessionFactory; @@ -31,7 +29,9 @@ import java.time.LocalDateTime; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @SpringJUnitConfig @@ -56,10 +56,13 @@ class IostorInvServiceLocalSpringTest { String id = iostorInvService.createOutbound(buildRequest()); assertNotNull(id); + assertFalse(id.isBlank()); jdbcTemplate.queryForObject("SELECT * FROM wms_iostorinv WHERE iostorinv_id = ?", (rs, rowNum) -> { assertEquals("OUT-TEST-0001", rs.getString("bill_code")); assertEquals("OUT", rs.getString("io_type")); assertEquals("销售出库", rs.getString("bill_type")); + assertEquals(LocalDateTime.of(2026, 7, 22, 0, 0), + rs.getTimestamp("biz_date").toLocalDateTime()); assertEquals("STOR-01", rs.getString("stor_id")); assertEquals("生成", rs.getString("bill_status")); assertEquals("主表备注", rs.getString("remark")); @@ -70,19 +73,27 @@ class IostorInvServiceLocalSpringTest { List details = jdbcTemplate.query( "SELECT * FROM wms_iostorinvdtl WHERE iostorinv_id = ? ORDER BY seq_no", - (rs, rowNum) -> new DetailRow(rs.getInt("seq_no"), rs.getString("material_code"), + (rs, rowNum) -> new DetailRow(rs.getString("iostorinvdtl_id"), rs.getString("iostorinv_id"), + rs.getInt("seq_no"), rs.getString("material_code"), rs.getString("material_id"), rs.getString("pcsn"), rs.getBigDecimal("plan_qty"), rs.getBigDecimal("assign_qty"), rs.getBigDecimal("unassign_qty"), rs.getString("qty_unit_id"), rs.getString("qty_unit_name"), rs.getString("source_bill_code"), rs.getString("source_bill_type"), rs.getString("source_billdtl_id"), rs.getString("remark")), id); - assertEquals(List.of( - new DetailRow(1, "MAT-01", "MID-01", "PCSN-01", new BigDecimal("12.500"), - BigDecimal.ZERO.setScale(3), new BigDecimal("12.500"), "KG", "千克", - "SRC-01", "ORDER", "SRC-DTL-01", "明细一"), - new DetailRow(2, "MAT-02", "MID-02", "PCSN-02", new BigDecimal("7.250"), - BigDecimal.ZERO.setScale(3), new BigDecimal("7.250"), "KG", "千克", - "SRC-02", "ORDER", "SRC-DTL-02", "明细二")), details); + assertEquals(2, details.size()); + assertNotNull(details.get(0).detailId()); + assertNotNull(details.get(1).detailId()); + assertFalse(details.get(0).detailId().isBlank()); + assertFalse(details.get(1).detailId().isBlank()); + assertNotEquals(details.get(0).detailId(), details.get(1).detailId()); + assertEquals(id, details.get(0).headerId()); + assertEquals(id, details.get(1).headerId()); + assertEquals(new DetailRow(details.get(0).detailId(), id, 1, "MAT-01", "MID-01", "PCSN-01", + new BigDecimal("12.500"), BigDecimal.ZERO.setScale(3), new BigDecimal("12.500"), + "KG", "千克", "SRC-01", "ORDER", "SRC-DTL-01", "明细一"), details.get(0)); + assertEquals(new DetailRow(details.get(1).detailId(), id, 2, "MAT-02", "MID-02", "PCSN-02", + new BigDecimal("7.250"), BigDecimal.ZERO.setScale(3), new BigDecimal("7.250"), + "KG", "千克", "SRC-02", "ORDER", "SRC-DTL-02", "明细二"), details.get(1)); } @Test @@ -97,6 +108,17 @@ class IostorInvServiceLocalSpringTest { assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); } + @Test + void shouldRollbackHeaderAndFirstDetailWhenSecondDetailInsertFails() { + IostorInvCreateReqVO request = buildRequest(); + request.getDetails().get(1).setSourceBillCode(null); + + assertThrows(RuntimeException.class, () -> iostorInvService.createOutbound(request)); + + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + private IostorInvCreateReqVO buildRequest() { IostorInvCreateReqVO request = new IostorInvCreateReqVO(); request.setBillType("销售出库"); @@ -123,7 +145,8 @@ class IostorInvServiceLocalSpringTest { return detail; } - private record DetailRow(Integer seqNo, String materialCode, String materialId, String pcsn, + private record DetailRow(String detailId, String headerId, Integer seqNo, String materialCode, + String materialId, String pcsn, BigDecimal planQty, BigDecimal assignQty, BigDecimal unassignQty, String qtyUnitId, String qtyUnitName, String sourceBillCode, String sourceBillType, String sourceBilldtlId, String remark) { @@ -144,9 +167,6 @@ class IostorInvServiceLocalSpringTest { SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); - GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig(); - dbConfig.setKeyGenerators(List.of(new H2KeyGenerator())); - factory.setGlobalConfig(new GlobalConfig().setDbConfig(dbConfig)); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); factory.setConfiguration(configuration); diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql index 8d3f1e65..564c3b5b 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -1,11 +1,5 @@ DROP TABLE IF EXISTS wms_iostorinvdtl; DROP TABLE IF EXISTS wms_iostorinv; -DROP SEQUENCE IF EXISTS wms_iostorinvdtl_seq; -DROP SEQUENCE IF EXISTS wms_iostorinv_seq; - -CREATE SEQUENCE wms_iostorinv_seq START WITH 1; -CREATE SEQUENCE wms_iostorinvdtl_seq START WITH 1; - CREATE TABLE wms_iostorinv ( iostorinv_id VARCHAR(64) PRIMARY KEY, bill_code VARCHAR(64), io_type VARCHAR(32), bill_type VARCHAR(64), biz_date TIMESTAMP, @@ -22,7 +16,7 @@ CREATE TABLE wms_iostorinvdtl ( iostorinvdtl_id VARCHAR(64) PRIMARY KEY, iostorinv_id VARCHAR(64), seq_no INT, pcsn VARCHAR(128), bill_status VARCHAR(32), qty_unit_id VARCHAR(64), qty_unit_name VARCHAR(64), plan_qty DECIMAL(18, 3), real_qty DECIMAL(18, 3), source_billdtl_id VARCHAR(64), - source_bill_type VARCHAR(64), source_bill_code VARCHAR(64), source_bill_table VARCHAR(128), + source_bill_type VARCHAR(64), source_bill_code VARCHAR(64) NOT NULL, source_bill_table VARCHAR(128), remark VARCHAR(255), assign_qty DECIMAL(18, 3), unassign_qty DECIMAL(18, 3), material_code VARCHAR(64), source_load_port VARCHAR(64), callback_strategy VARCHAR(255), material_id VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, From c4c8911a80eb1402532977cbf74d69c8d441914f Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:00:04 +0800 Subject: [PATCH 12/24] =?UTF-8?q?fix:=20=E5=BC=BA=E5=8C=96=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E4=BA=8B=E5=8A=A1=E4=B8=8E=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/wms/enums/ErrorCodeConstants.java | 2 +- .../iostorinv/IostorInvServiceImpl.java | 17 ++- .../IostorInvServiceLocalSpringTest.java | 101 +++++++++++++++++- .../sql/iostorinv/create-outbound.sql | 2 +- 4 files changed, 112 insertions(+), 10 deletions(-) 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 955f2fdb..1e263ad5 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 @@ -12,7 +12,7 @@ public interface ErrorCodeConstants { // ================ 出入库表相关错误码 ================= ErrorCode IOSTOR_INV_NOT_EXISTS = new ErrorCode(4, "出入库单主表不存在"); - ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(7, "单据号生成失败"); + ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(6_000_4, "单据号生成失败"); ErrorCode IOSTORINV_DTL_NOT_EXISTS = new ErrorCode(5, "出入库单明细不存在"); 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 ffccd527..60ec58d1 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 @@ -2,6 +2,7 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; +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; @@ -53,12 +54,20 @@ public class IostorInvServiceImpl implements IostorInvService { @Override @Transactional(rollbackFor = Exception.class) public String createOutbound(IostorInvCreateReqVO reqVO) { + String ruleCode = "IO_CODE"; CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO(); - codeGenerateReqDTO.setRuleCode("IO_CODE"); - CommonResult codeResult = codeGenApi.generate(codeGenerateReqDTO); - if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) { + codeGenerateReqDTO.setRuleCode(ruleCode); + CommonResult codeResult; + try { + codeResult = codeGenApi.generate(codeGenerateReqDTO); + } catch (RuntimeException ex) { + log.error("[createOutbound][生成出库单号异常,规则编码:{}]", ruleCode, ex); + // 当前统一业务异常不支持 cause,远程异常堆栈已在此处完整记录。 + throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); + } + if (codeResult == null || !codeResult.isSuccess() || StrUtil.isBlank(codeResult.getData())) { String remoteMessage = codeResult == null ? "返回结果为空" : codeResult.getMsg(); - log.error("[createOutbound][生成出库单号失败,远程信息:{}]", remoteMessage); + log.error("[createOutbound][生成出库单号失败,规则编码:{},远程信息:{}]", ruleCode, remoteMessage); throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); } diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index 5b3f3365..3d68e44f 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -7,12 +7,17 @@ import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import jakarta.annotation.Resource; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; import org.apache.ibatis.session.SqlSessionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DriverManagerDataSource; @@ -33,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.IOSTOR_INV_CODE_GENERATE_FAILED; @SpringJUnitConfig @ContextConfiguration(classes = IostorInvServiceLocalSpringTest.TestConfiguration.class) @@ -45,10 +51,14 @@ class IostorInvServiceLocalSpringTest { private JdbcTemplate jdbcTemplate; @Resource private TestCodeGenApi codeGenApi; + @Resource + private SecondDetailInsertFailureAspect detailInsertFailureAspect; @BeforeEach void resetCodeGenApi() { codeGenApi.setResult(CommonResult.success("OUT-TEST-0001")); + codeGenApi.setFailure(null); + detailInsertFailureAspect.disable(); } @Test @@ -104,17 +114,43 @@ class IostorInvServiceLocalSpringTest { () -> iostorInvService.createOutbound(buildRequest())); assertEquals("单据号生成失败", exception.getMessage()); + assertEquals(IOSTOR_INV_CODE_GENERATE_FAILED.getCode(), exception.getCode()); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); } @Test - void shouldRollbackHeaderAndFirstDetailWhenSecondDetailInsertFails() { - IostorInvCreateReqVO request = buildRequest(); - request.getDetails().get(1).setSourceBillCode(null); + void shouldRejectBlankGeneratedCodeWithoutInsertingAnything() { + codeGenApi.setResult(CommonResult.success(" ")); - assertThrows(RuntimeException.class, () -> iostorInvService.createOutbound(request)); + ServiceException exception = assertThrows(ServiceException.class, + () -> iostorInvService.createOutbound(buildRequest())); + assertEquals(IOSTOR_INV_CODE_GENERATE_FAILED.getCode(), exception.getCode()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + + @Test + void shouldConvertRemoteCodeGenerationExceptionToBusinessError() { + codeGenApi.setFailure(new IllegalStateException("远程连接失败")); + + ServiceException exception = assertThrows(ServiceException.class, + () -> iostorInvService.createOutbound(buildRequest())); + + assertEquals(IOSTOR_INV_CODE_GENERATE_FAILED.getCode(), exception.getCode()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + + @Test + void shouldRollbackRealInsertsWhenInjectedSecondDetailFailureOccurs() { + detailInsertFailureAspect.failOnSecondInsert(); + + assertThrows(DataAccessResourceFailureException.class, + () -> iostorInvService.createOutbound(buildRequest())); + + assertEquals(1, detailInsertFailureAspect.getRowsVisibleBeforeFailure()); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); } @@ -154,6 +190,7 @@ class IostorInvServiceLocalSpringTest { @Configuration(proxyBeanMethods = false) @EnableTransactionManagement + @EnableAspectJAutoProxy @MapperScan(basePackages = {"cn.code.nl.module.wms.dal.mysql.iostorinv", "cn.code.nl.module.wms.dal.mysql.iostorinvdtl"}) static class TestConfiguration { @@ -197,18 +234,31 @@ class IostorInvServiceLocalSpringTest { TestCodeGenApi codeGenApi() { return new TestCodeGenApi(); } + + @Bean + SecondDetailInsertFailureAspect detailInsertFailureAspect(JdbcTemplate jdbcTemplate) { + return new SecondDetailInsertFailureAspect(jdbcTemplate); + } } static class TestCodeGenApi implements CodeGenApi { private CommonResult result; + private RuntimeException failure; void setResult(CommonResult result) { this.result = result; } + void setFailure(RuntimeException failure) { + this.failure = failure; + } + @Override public CommonResult generate(CodeGenerateReqDTO reqDTO) { assertEquals("IO_CODE", reqDTO.getRuleCode()); + if (failure != null) { + throw failure; + } return result; } @@ -217,4 +267,47 @@ class IostorInvServiceLocalSpringTest { throw new UnsupportedOperationException(); } } + + /** 在第二次明细真实插入的调用点注入数据库访问故障,用于验证事务回滚。 */ + @Aspect + static class SecondDetailInsertFailureAspect { + private final JdbcTemplate jdbcTemplate; + private boolean enabled; + private int insertCount; + private int rowsVisibleBeforeFailure; + + SecondDetailInsertFailureAspect(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + void failOnSecondInsert() { + enabled = true; + insertCount = 0; + rowsVisibleBeforeFailure = 0; + } + + void disable() { + enabled = false; + insertCount = 0; + rowsVisibleBeforeFailure = 0; + } + + int getRowsVisibleBeforeFailure() { + return rowsVisibleBeforeFailure; + } + + @Around("bean(iostorinvDtlMapper) && execution(* insert(..))") + Object injectFailureOnSecondInsert(ProceedingJoinPoint joinPoint) throws Throwable { + if (!enabled) { + return joinPoint.proceed(); + } + insertCount++; + if (insertCount == 2) { + rowsVisibleBeforeFailure = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class); + throw new DataAccessResourceFailureException("测试注入:第二条明细写入失败"); + } + return joinPoint.proceed(); + } + } } diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql index 564c3b5b..d684f115 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -16,7 +16,7 @@ CREATE TABLE wms_iostorinvdtl ( iostorinvdtl_id VARCHAR(64) PRIMARY KEY, iostorinv_id VARCHAR(64), seq_no INT, pcsn VARCHAR(128), bill_status VARCHAR(32), qty_unit_id VARCHAR(64), qty_unit_name VARCHAR(64), plan_qty DECIMAL(18, 3), real_qty DECIMAL(18, 3), source_billdtl_id VARCHAR(64), - source_bill_type VARCHAR(64), source_bill_code VARCHAR(64) NOT NULL, source_bill_table VARCHAR(128), + source_bill_type VARCHAR(64), source_bill_code VARCHAR(64), source_bill_table VARCHAR(128), remark VARCHAR(255), assign_qty DECIMAL(18, 3), unassign_qty DECIMAL(18, 3), material_code VARCHAR(64), source_load_port VARCHAR(64), callback_strategy VARCHAR(255), material_id VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, From 0eeca4694594aeb8478a987fd89479a53bce82b4 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:03:46 +0800 Subject: [PATCH 13/24] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E5=89=8D=E7=AB=AF=E8=81=9A=E5=90=88=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E6=B1=87=E6=80=BB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/wms/iostorinv/index.ts | 87 ++++++++++++++++++- .../iostorinv/modules/detail-summary.test.ts | 15 ++++ .../wms/iostorinv/modules/detail-summary.ts | 14 +++ 3 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts 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 6afad210..5c9cc4ae 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 @@ -1,6 +1,7 @@ -import type { PageParam, PageResult } from '@vben/request'; import type { Dayjs } from 'dayjs'; +import type { PageParam, PageResult } from '@vben/request'; + import { requestClient } from '#/api/request'; export namespace WmsIostorInvApi { @@ -10,7 +11,7 @@ export namespace WmsIostorInvApi { billCode?: string; // 单据编号 ioType?: string; // 出入类型 billType?: string; // 单据类型 - bizDate?: string | Dayjs; // 业务日期 + bizDate?: Dayjs | string; // 业务日期 storId: string; // 仓库标识 storCode: string; // 仓库编码 storName: string; // 仓库名称 @@ -24,15 +25,70 @@ export namespace WmsIostorInvApi { remark: string; // 备注 createMode?: string; // 生成方式 disOptid: string; // 分配人 - disTime?: string | Dayjs; // 分配时间 + disTime?: Dayjs | string; // 分配时间 confirmOptid: string; // 确认人 - confirmTime?: string | Dayjs; // 确认时间 + confirmTime?: Dayjs | string; // 确认时间 sysdeptid: string; // 部门ID syscompanyid: string; // 公司ID isUpload?: string; // 是否已上传 uploadOptid: string; // 回传人 uploadTime: string; // 回传时间 } + + /** 出库单可持久化明细 */ + export interface OutboundDetail { + materialCode: string; // 物料编码 + materialId?: string; // 物料标识 + pcsn?: string; // 批次序列号 + planQty: number; // 出库重量 + qtyUnitId?: string; // 数量单位标识 + qtyUnitName?: string; // 数量单位名称 + sourceBillCode?: string; // 来源单据编号 + sourceBillType?: string; // 来源单据类型 + sourceBilldtlId?: string; // 来源单据明细标识 + remark?: string; // 备注 + } + + /** 出库单新增请求 */ + export interface OutboundCreateReq { + billType: string; // 单据类型 + storId: string; // 仓库标识 + bizDate: Dayjs | string; // 业务日期 + remark?: string; // 备注 + details: OutboundDetail[]; // 出库单明细 + } + + /** 可用库存 */ + export interface AvailableInventory { + vehicleCode: string; // 箱号 + pcsn: string; // 批次 + materialId: string; // 物料标识 + materialCode: string; // 物料编码 + materialName: string; // 物料名称 + availableQty: number; // 可用数量 + qtyUnitId: string; // 计量单位标识 + qtyUnitName: string; // 计量单位名称 + extCode: string; // 来源单据号 + extType: string; // 来源单据类型 + extDtlCode: string; // 来源单据明细号 + storId: string; // 仓库标识 + storCode: string; // 仓库编码 + storName: string; // 仓库名称 + } + + /** 可用库存分页请求 */ + export interface AvailableInventoryPageReq extends PageParam { + storId: string; // 仓库标识 + materialCode?: string; // 物料编码 + vehicleCode?: string; // 箱号 + pcsn?: string; // 批次 + } + + /** 按箱号展开可用库存请求 */ + export interface ExpandAvailableInventoryReq { + storId: string; // 仓库标识 + vehicleCodes: string[]; // 箱号列表 + } } /** 查询出入库单主表分页 */ @@ -77,4 +133,27 @@ export function exportIostorInv(params: any) { return requestClient.download('/wms/iostor-inv/export-excel', { params }); } +/** 查询可用库存分页 */ +export function getAvailableInventoryPage( + params: WmsIostorInvApi.AvailableInventoryPageReq, +) { + return requestClient.get< + PageResult + >('/wms/iostor-inv/availableInventoryPage', { params }); +} + +/** 按箱号展开可用库存 */ +export function expandAvailableInventory( + data: WmsIostorInvApi.ExpandAvailableInventoryReq, +) { + return requestClient.post( + '/wms/iostor-inv/expandAvailableInventory', + data, + ); +} + +/** 创建出库单 */ +export function createOutbound(data: WmsIostorInvApi.OutboundCreateReq) { + return requestClient.post('/wms/iostor-inv/createOutbound', data); +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts new file mode 100644 index 00000000..03242aa8 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { summarizeDetails } from './detail-summary'; + +describe('summarizeDetails', () => { + it('汇总明细数量和总重量', () => { + expect( + summarizeDetails([{ planQty: 12.5 }, { planQty: 7.25 }]), + ).toEqual({ detailCount: 2, totalWeight: 19.75 }); + }); + + it('空明细返回零值', () => { + expect(summarizeDetails([])).toEqual({ detailCount: 0, totalWeight: 0 }); + }); +}); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts new file mode 100644 index 00000000..7901b3ec --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts @@ -0,0 +1,14 @@ +export interface OutboundDetailLike { + planQty: number; +} + +/** 汇总出库明细数量和重量 */ +export function summarizeDetails(details: OutboundDetailLike[]) { + return { + detailCount: details.length, + totalWeight: details.reduce( + (total, detail) => total + Number(detail.planQty), + 0, + ), + }; +} From 36d0be150095da3e4b6906135433beaee1f03edf Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:08:16 +0800 Subject: [PATCH 14/24] =?UTF-8?q?fix:=20=E6=94=B6=E7=B4=A7=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E8=AF=B7=E6=B1=82=E4=B8=8E=E6=B1=87=E6=80=BB=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/wms/iostorinv/index.ts | 21 +++++++++---------- .../iostorinv/modules/detail-summary.test.ts | 12 +++++++++++ .../wms/iostorinv/modules/detail-summary.ts | 10 ++++----- 3 files changed, 27 insertions(+), 16 deletions(-) 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 5c9cc4ae..fd888ccf 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 @@ -53,7 +53,7 @@ export namespace WmsIostorInvApi { export interface OutboundCreateReq { billType: string; // 单据类型 storId: string; // 仓库标识 - bizDate: Dayjs | string; // 业务日期 + bizDate: number; // 业务日期(毫秒时间戳) remark?: string; // 备注 details: OutboundDetail[]; // 出库单明细 } @@ -61,19 +61,19 @@ export namespace WmsIostorInvApi { /** 可用库存 */ export interface AvailableInventory { vehicleCode: string; // 箱号 - pcsn: string; // 批次 + pcsn?: null | string; // 批次 materialId: string; // 物料标识 materialCode: string; // 物料编码 - materialName: string; // 物料名称 + materialName?: null | string; // 物料名称 availableQty: number; // 可用数量 - qtyUnitId: string; // 计量单位标识 - qtyUnitName: string; // 计量单位名称 - extCode: string; // 来源单据号 - extType: string; // 来源单据类型 - extDtlCode: string; // 来源单据明细号 + qtyUnitId?: null | string; // 计量单位标识 + qtyUnitName?: null | string; // 计量单位名称 + extCode?: null | string; // 来源单据号 + extType?: null | string; // 来源单据类型 + extDtlCode?: null | string; // 来源单据明细号 storId: string; // 仓库标识 - storCode: string; // 仓库编码 - storName: string; // 仓库名称 + storCode?: null | string; // 仓库编码 + storName?: null | string; // 仓库名称 } /** 可用库存分页请求 */ @@ -156,4 +156,3 @@ export function expandAvailableInventory( export function createOutbound(data: WmsIostorInvApi.OutboundCreateReq) { return requestClient.post('/wms/iostor-inv/createOutbound', data); } - diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts index 03242aa8..76663e17 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts @@ -12,4 +12,16 @@ describe('summarizeDetails', () => { it('空明细返回零值', () => { expect(summarizeDetails([])).toEqual({ detailCount: 0, totalWeight: 0 }); }); + + it('忽略编辑态的空值和非法重量', () => { + expect( + summarizeDetails([ + { planQty: undefined }, + { planQty: '' }, + { planQty: 'invalid' }, + { planQty: Number.NaN }, + { planQty: null }, + ]), + ).toEqual({ detailCount: 5, totalWeight: 0 }); + }); }); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts index 7901b3ec..d85f95a0 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts @@ -1,14 +1,14 @@ export interface OutboundDetailLike { - planQty: number; + planQty?: null | number | string; } /** 汇总出库明细数量和重量 */ export function summarizeDetails(details: OutboundDetailLike[]) { return { detailCount: details.length, - totalWeight: details.reduce( - (total, detail) => total + Number(detail.planQty), - 0, - ), + totalWeight: details.reduce((total, detail) => { + const planQty = Number(detail.planQty); + return total + (Number.isFinite(planQty) ? planQty : 0); + }, 0), }; } From d4a28480f1d36d3783844bdc25bd1d43673c077f Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:15:47 +0800 Subject: [PATCH 15/24] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=BA=93=E5=AD=98=E9=80=89=E6=8B=A9=E5=92=8C=E6=89=8B?= =?UTF-8?q?=E5=B7=A5=E6=B1=87=E6=80=BB=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/wms/iostorinv/index.ts | 7 + .../iostorinv/modules/detail-builders.test.ts | 111 +++++++++++++++ .../wms/iostorinv/modules/detail-builders.ts | 61 +++++++++ .../iostorinv/modules/inventory-select.vue | 129 ++++++++++++++++++ .../wms/iostorinv/modules/manual-detail.vue | 110 +++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue 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 fd888ccf..d29c44f7 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 @@ -49,6 +49,13 @@ export namespace WmsIostorInvApi { remark?: string; // 备注 } + /** 出库明细页面展示模型(提交时应转换为 OutboundDetail) */ + export interface OutboundDisplayDetail extends OutboundDetail { + materialName?: string; + sapBatchNo?: string; + vehicleCode?: string; + } + /** 出库单新增请求 */ export interface OutboundCreateReq { billType: string; // 单据类型 diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts new file mode 100644 index 00000000..2c8ac410 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildManualDetail, + collectVehicleCodes, + mapExpandedInventory, +} from './detail-builders'; + +describe('mapExpandedInventory', () => { + it('逐条映射展开后的全部子卷,且不把 pcsn 当作 SAP 批次', () => { + const rows = mapExpandedInventory([ + { + availableQty: 12.5, + extCode: 'OUT-1', + extDtlCode: 'DTL-1', + extType: 'SALE', + materialCode: 'M-1', + materialId: '1', + materialName: '物料一', + pcsn: 'P-1', + qtyUnitId: 'kg', + qtyUnitName: '千克', + storId: 'S-1', + vehicleCode: 'BOX-1', + }, + { + availableQty: 8, + materialCode: 'M-1', + materialId: '1', + pcsn: 'P-2', + storId: 'S-1', + vehicleCode: 'BOX-1', + }, + ]); + + expect(rows).toHaveLength(2); + expect(rows[0]).toMatchObject({ + pcsn: 'P-1', + planQty: 12.5, + sapBatchNo: undefined, + sourceBillCode: 'OUT-1', + sourceBillType: 'SALE', + sourceBilldtlId: 'DTL-1', + }); + expect(rows[1]?.pcsn).toBe('P-2'); + }); +}); + +describe('buildManualDetail', () => { + it('构造手工汇总行并显式清空箱号、子卷号和 SAP 批次', () => { + expect( + buildManualDetail( + { + baseUnitId: 7, + materialCode: 'M-1', + materialId: 1, + materialName: '物料一', + }, + 20, + '备注', + ), + ).toMatchObject({ + materialCode: 'M-1', + materialId: '1', + materialName: '物料一', + pcsn: undefined, + planQty: 20, + qtyUnitId: '7', + qtyUnitName: undefined, + remark: '备注', + sapBatchNo: undefined, + vehicleCode: undefined, + }); + }); +}); + +describe('collectVehicleCodes', () => { + it('去重并过滤空箱号', () => { + expect( + collectVehicleCodes([ + { vehicleCode: ' BOX-1 ' }, + { vehicleCode: '' }, + { vehicleCode: 'BOX-1' }, + { vehicleCode: 'BOX-2' }, + ]), + ).toEqual(['BOX-1', 'BOX-2']); + }); + + it('允许 500 个箱号并拒绝 501 个', () => { + expect( + collectVehicleCodes( + Array.from({ length: 500 }, (_, index) => ({ + vehicleCode: `BOX-${index}`, + })), + ), + ).toHaveLength(500); + expect(() => + collectVehicleCodes( + Array.from({ length: 501 }, (_, index) => ({ + vehicleCode: `BOX-${index}`, + })), + ), + ).toThrow('箱号数量不能超过 500 个'); + }); + + it('拒绝没有有效箱号的选择', () => { + expect(() => collectVehicleCodes([{ vehicleCode: ' ' }])).toThrow( + '请选择包含有效箱号的库存', + ); + }); +}); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts new file mode 100644 index 00000000..1d8b2450 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts @@ -0,0 +1,61 @@ +import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; + +/** 物料选择弹窗返回行中,手工明细所需的真实字段 */ +export interface MaterialSelection { + baseUnitId?: number; + materialCode?: string; + materialId: number | string; + materialName?: string; +} + +export function mapExpandedInventory( + inventory: WmsIostorInvApi.AvailableInventory[], +): WmsIostorInvApi.OutboundDisplayDetail[] { + return inventory.map((row) => ({ + materialCode: row.materialCode, + materialId: row.materialId, + materialName: row.materialName ?? undefined, + pcsn: row.pcsn ?? undefined, + planQty: row.availableQty, + qtyUnitId: row.qtyUnitId ?? undefined, + qtyUnitName: row.qtyUnitName ?? undefined, + sapBatchNo: undefined, + sourceBillCode: row.extCode ?? undefined, + sourceBillType: row.extType ?? undefined, + sourceBilldtlId: row.extDtlCode ?? undefined, + vehicleCode: row.vehicleCode, + })); +} + +export function collectVehicleCodes(rows: Array<{ vehicleCode?: string }>) { + const vehicleCodes = [ + ...new Set(rows.map(({ vehicleCode }) => vehicleCode?.trim()).filter(Boolean)), + ] as string[]; + if (vehicleCodes.length === 0) { + throw new Error('请选择包含有效箱号的库存'); + } + if (vehicleCodes.length > 500) { + throw new Error('箱号数量不能超过 500 个'); + } + return vehicleCodes; +} + +export function buildManualDetail( + material: MaterialSelection, + planQty: number, + remark?: string, +): WmsIostorInvApi.OutboundDisplayDetail { + return { + materialCode: material.materialCode ?? '', + materialId: String(material.materialId), + materialName: material.materialName, + pcsn: undefined, + planQty, + qtyUnitId: + material.baseUnitId === undefined ? undefined : String(material.baseUnitId), + qtyUnitName: undefined, + remark: remark || undefined, + sapBatchNo: undefined, + vehicleCode: undefined, + }; +} diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue new file mode 100644 index 00000000..207815ed --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue @@ -0,0 +1,129 @@ + + + + + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue new file mode 100644 index 00000000..b9da5bd1 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue @@ -0,0 +1,110 @@ + + + + + + 选择物料 + + + + + From 6210286518d1f6f8a3bb3e22c010ba0b17f6783c Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:23:13 +0800 Subject: [PATCH 16/24] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E5=BA=93?= =?UTF-8?q?=E5=AD=98=E8=B7=A8=E9=A1=B5=E9=80=89=E6=8B=A9=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/AvailableInventoryRespVO.java | 2 + .../iostorinvdtl/IostorinvDtlMapper.xml | 1 + .../iostorinvdtl/IostorinvDtlMapperTest.java | 2 + .../src/api/base/materialbase/index.ts | 2 +- .../src/api/wms/iostorinv/index.ts | 1 + .../iostorinv/modules/detail-builders.test.ts | 18 +++++ .../wms/iostorinv/modules/detail-builders.ts | 21 +++-- .../iostorinv/modules/inventory-select.vue | 76 ++++++++++++++----- 8 files changed, 98 insertions(+), 25 deletions(-) diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java index b6ba82e4..9046b1cb 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java @@ -9,6 +9,8 @@ import java.math.BigDecimal; @Data public class AvailableInventoryRespVO { + @Schema(description = "组盘记录标识") + private Long groupId; @Schema(description = "箱号") private String vehicleCode; @Schema(description = "批次") diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml index 6b2947f3..1612604d 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml @@ -3,6 +3,7 @@ + gp.group_id AS group_id, gp.vehicle_code AS vehicle_code, gp.pcsn AS pcsn, gp.material_id AS material_id, diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java index 8b673002..fc783563 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java @@ -52,6 +52,7 @@ class IostorinvDtlMapperTest { List expanded = service.expandAvailableInventory(expandReqVO); assertEquals(2, expanded.size()); + assertEquals(List.of(1L, 2L), expanded.stream().map(AvailableInventoryRespVO::getGroupId).toList()); assertTrue(expanded.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode()))); assertTrue(expanded.stream().allMatch(item -> "STOR-001".equals(item.getStorId()))); assertEquals(2, expanded.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); @@ -75,6 +76,7 @@ class IostorinvDtlMapperTest { assertEquals(1L, page.getTotal()); assertEquals("PCSN-002", page.getList().get(0).getPcsn()); + assertEquals(2L, page.getList().get(0).getGroupId()); assertEquals("材料一", page.getList().get(0).getMaterialName()); } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts index 4c6091e1..80a2090a 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts @@ -1,11 +1,11 @@ import type { PageParam, PageResult } from '@vben/request'; -import type { Dayjs } from 'dayjs'; import { requestClient } from '#/api/request'; export namespace BaseMaterialBaseApi { /** 物料基本信息信息 */ export interface MaterialBase { + materialId: number; // 物料标识 materialCode?: string; // 物料编码 materialName?: string; // 物料名称 materialSpec: string; // 规格 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 d29c44f7..dd930de2 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 @@ -67,6 +67,7 @@ export namespace WmsIostorInvApi { /** 可用库存 */ export interface AvailableInventory { + groupId: string; // 组盘记录标识 vehicleCode: string; // 箱号 pcsn?: null | string; // 批次 materialId: string; // 物料标识 diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts index 2c8ac410..6c52b5da 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts @@ -4,6 +4,7 @@ import { buildManualDetail, collectVehicleCodes, mapExpandedInventory, + mergeInventorySelections, } from './detail-builders'; describe('mapExpandedInventory', () => { @@ -11,6 +12,7 @@ describe('mapExpandedInventory', () => { const rows = mapExpandedInventory([ { availableQty: 12.5, + groupId: '101', extCode: 'OUT-1', extDtlCode: 'DTL-1', extType: 'SALE', @@ -25,6 +27,7 @@ describe('mapExpandedInventory', () => { }, { availableQty: 8, + groupId: '102', materialCode: 'M-1', materialId: '1', pcsn: 'P-2', @@ -46,6 +49,21 @@ describe('mapExpandedInventory', () => { }); }); +describe('mergeInventorySelections', () => { + it('合并当前页和跨页保留记录,并按 groupId 去重', () => { + const reserved = [{ groupId: '1', vehicleCode: 'BOX-1' }]; + const current = [ + { groupId: '1', vehicleCode: 'BOX-1' }, + { groupId: '2', vehicleCode: 'BOX-2' }, + ]; + + expect(mergeInventorySelections(current, reserved)).toEqual([ + reserved[0], + current[1], + ]); + }); +}); + describe('buildManualDetail', () => { it('构造手工汇总行并显式清空箱号、子卷号和 SAP 批次', () => { expect( diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts index 1d8b2450..b35b5cd4 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts @@ -1,12 +1,11 @@ +import type { BaseMaterialBaseApi } from '#/api/base/materialbase'; import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; /** 物料选择弹窗返回行中,手工明细所需的真实字段 */ -export interface MaterialSelection { - baseUnitId?: number; - materialCode?: string; - materialId: number | string; - materialName?: string; -} +export type MaterialSelection = Pick< + BaseMaterialBaseApi.MaterialBase, + 'baseUnitId' | 'materialCode' | 'materialId' | 'materialName' +>; export function mapExpandedInventory( inventory: WmsIostorInvApi.AvailableInventory[], @@ -40,6 +39,16 @@ export function collectVehicleCodes(rows: Array<{ vehicleCode?: string }>) { return vehicleCodes; } +/** 合并当前页与 VXE 跨页保留记录,以组盘记录标识去重 */ +export function mergeInventorySelections( + current: T[], + reserved: T[], +) { + const selected = new Map(); + [...reserved, ...current].forEach((row) => selected.set(row.groupId, row)); + return [...selected.values()]; +} + export function buildManualDetail( material: MaterialSelection, planQty: number, diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue index 207815ed..c402c769 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue @@ -14,16 +14,26 @@ import { getAvailableInventoryPage, } from '#/api/wms/iostorinv'; -import { collectVehicleCodes, mapExpandedInventory } from './detail-builders'; +import { + collectVehicleCodes, + mapExpandedInventory, + mergeInventorySelections, +} from './detail-builders'; -interface InventoryGridRow extends WmsIostorInvApi.AvailableInventory { - selectionKey: string; -} +type InventoryGridRow = WmsIostorInvApi.AvailableInventory; const emit = defineEmits<{ select: [rows: WmsIostorInvApi.OutboundDisplayDetail[]]; }>(); const storId = ref(''); +const modalOpen = ref(false); +let openToken = 0; + +async function clearGridState() { + await gridApi.grid.clearCheckboxRow(); + await gridApi.grid.clearCheckboxReserve(); + await gridApi.grid.loadData([]); +} const [Grid, gridApi] = useVbenVxeGrid({ formOptions: { @@ -60,33 +70,44 @@ const [Grid, gridApi] = useVbenVxeGrid({ ], height: 'auto', keepSource: true, + checkboxConfig: { highlight: true, reserve: true }, proxyConfig: { ajax: { query: async ({ page }, formValues) => { + const requestToken = openToken; + const requestStorId = storId.value; + if (!modalOpen.value || !requestStorId) return { list: [], total: 0 }; const result = await getAvailableInventoryPage({ ...formValues, pageNo: page.currentPage, pageSize: page.pageSize, - storId: storId.value, + storId: requestStorId, }); - return { - ...result, - list: result.list.map((row) => ({ - ...row, - selectionKey: `${row.vehicleCode}\u0000${row.pcsn ?? ''}\u0000${row.materialId}`, - })), - }; + if ( + requestToken !== openToken || + !modalOpen.value || + requestStorId !== storId.value + ) { + return { list: [], total: 0 }; + } + return result; }, }, }, - rowConfig: { isHover: true, keyField: 'selectionKey' }, + rowConfig: { isHover: true, keyField: 'groupId' }, toolbarConfig: { refresh: true, search: true }, } as VxeTableGridOptions, }); const [Modal, modalApi] = useVbenModal({ async onConfirm() { - const selected = gridApi.grid.getCheckboxRecords() as InventoryGridRow[]; + const confirmToken = openToken; + const confirmStorId = storId.value; + if (!modalOpen.value || !confirmStorId) return; + const selected = mergeInventorySelections( + gridApi.grid.getCheckboxRecords() as InventoryGridRow[], + gridApi.grid.getCheckboxReserveRecords() as InventoryGridRow[], + ); if (selected.length === 0) { message.warning('请至少选择一条库存信息'); return; @@ -101,22 +122,41 @@ const [Modal, modalApi] = useVbenModal({ modalApi.lock(); try { const expanded = await expandAvailableInventory({ - storId: storId.value, + storId: confirmStorId, vehicleCodes, }); + if ( + confirmToken !== openToken || + !modalOpen.value || + confirmStorId !== storId.value + ) { + return; + } emit('select', mapExpandedInventory(expanded)); await modalApi.close(); } finally { modalApi.unlock(); } }, - async onOpenChange(isOpen) { - if (!isOpen) { + async onOpenChange(open) { + const token = ++openToken; + if (!open) { + modalOpen.value = false; storId.value = ''; + await clearGridState(); return; } + modalOpen.value = true; + storId.value = ''; + await clearGridState(); + if (token !== openToken || !modalOpen.value) return; const data = modalApi.getData<{ storId: string }>(); - storId.value = data?.storId ?? ''; + if (!data?.storId) { + message.warning('请先选择仓库'); + await modalApi.close(); + return; + } + storId.value = data.storId; await gridApi.query(); }, }); From 733ef97a3bc4e7ed71e8ed056fd11ba2f2a6cfe8 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:26:58 +0800 Subject: [PATCH 17/24] =?UTF-8?q?fix:=20=E5=8F=96=E6=B6=88=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E5=BA=93=E5=AD=98=E5=88=86=E9=A1=B5=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/base/materialbase/index.ts | 2 +- .../src/api/wms/iostorinv/index.ts | 5 ++-- .../iostorinv/modules/detail-builders.test.ts | 10 +++---- .../wms/iostorinv/modules/detail-builders.ts | 8 ++++-- .../iostorinv/modules/inventory-select.vue | 28 +++++++++++++++---- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts index 80a2090a..e0b1b6bd 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/base/materialbase/index.ts @@ -5,7 +5,7 @@ import { requestClient } from '#/api/request'; export namespace BaseMaterialBaseApi { /** 物料基本信息信息 */ export interface MaterialBase { - materialId: number; // 物料标识 + materialId: number | string; // 物料标识 materialCode?: string; // 物料编码 materialName?: string; // 物料名称 materialSpec: string; // 规格 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 dd930de2..197dc02b 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 @@ -67,7 +67,7 @@ export namespace WmsIostorInvApi { /** 可用库存 */ export interface AvailableInventory { - groupId: string; // 组盘记录标识 + groupId: number | string; // 组盘记录标识 vehicleCode: string; // 箱号 pcsn?: null | string; // 批次 materialId: string; // 物料标识 @@ -144,10 +144,11 @@ export function exportIostorInv(params: any) { /** 查询可用库存分页 */ export function getAvailableInventoryPage( params: WmsIostorInvApi.AvailableInventoryPageReq, + signal?: AbortSignal, ) { return requestClient.get< PageResult - >('/wms/iostor-inv/availableInventoryPage', { params }); + >('/wms/iostor-inv/availableInventoryPage', { params, signal }); } /** 按箱号展开可用库存 */ diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts index 6c52b5da..f869193f 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.test.ts @@ -12,7 +12,7 @@ describe('mapExpandedInventory', () => { const rows = mapExpandedInventory([ { availableQty: 12.5, - groupId: '101', + groupId: 101, extCode: 'OUT-1', extDtlCode: 'DTL-1', extType: 'SALE', @@ -50,15 +50,15 @@ describe('mapExpandedInventory', () => { }); describe('mergeInventorySelections', () => { - it('合并当前页和跨页保留记录,并按 groupId 去重', () => { + it('合并跨页记录,并将 number/string 形式的相同 groupId 去重', () => { const reserved = [{ groupId: '1', vehicleCode: 'BOX-1' }]; const current = [ - { groupId: '1', vehicleCode: 'BOX-1' }, - { groupId: '2', vehicleCode: 'BOX-2' }, + { groupId: 1, vehicleCode: 'BOX-1' }, + { groupId: 2, vehicleCode: 'BOX-2' }, ]; expect(mergeInventorySelections(current, reserved)).toEqual([ - reserved[0], + current[0], current[1], ]); }); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts index b35b5cd4..df077843 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts @@ -40,12 +40,16 @@ export function collectVehicleCodes(rows: Array<{ vehicleCode?: string }>) { } /** 合并当前页与 VXE 跨页保留记录,以组盘记录标识去重 */ -export function mergeInventorySelections( +export function mergeInventorySelections< + T extends { groupId: number | string }, +>( current: T[], reserved: T[], ) { const selected = new Map(); - [...reserved, ...current].forEach((row) => selected.set(row.groupId, row)); + [...reserved, ...current].forEach((row) => + selected.set(String(row.groupId), row), + ); return [...selected.values()]; } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue index c402c769..1faaa765 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue @@ -28,11 +28,20 @@ const emit = defineEmits<{ const storId = ref(''); const modalOpen = ref(false); let openToken = 0; +let pageAbortController: AbortController | undefined; -async function clearGridState() { +function abortPageRequest() { + pageAbortController?.abort(); + pageAbortController = undefined; +} + +async function clearGridState(token: number) { await gridApi.grid.clearCheckboxRow(); + if (token !== openToken) return; await gridApi.grid.clearCheckboxReserve(); + if (token !== openToken) return; await gridApi.grid.loadData([]); + if (token !== openToken) return; } const [Grid, gridApi] = useVbenVxeGrid({ @@ -76,20 +85,26 @@ const [Grid, gridApi] = useVbenVxeGrid({ query: async ({ page }, formValues) => { const requestToken = openToken; const requestStorId = storId.value; - if (!modalOpen.value || !requestStorId) return { list: [], total: 0 }; + if (!modalOpen.value || !requestStorId) { + throw new DOMException('库存选择弹窗已关闭', 'AbortError'); + } + abortPageRequest(); + const controller = new AbortController(); + pageAbortController = controller; const result = await getAvailableInventoryPage({ ...formValues, pageNo: page.currentPage, pageSize: page.pageSize, storId: requestStorId, - }); + }, controller.signal); if ( requestToken !== openToken || !modalOpen.value || requestStorId !== storId.value ) { - return { list: [], total: 0 }; + throw new DOMException('库存分页请求已失效', 'AbortError'); } + if (pageAbortController === controller) pageAbortController = undefined; return result; }, }, @@ -140,15 +155,16 @@ const [Modal, modalApi] = useVbenModal({ }, async onOpenChange(open) { const token = ++openToken; + abortPageRequest(); if (!open) { modalOpen.value = false; storId.value = ''; - await clearGridState(); + await clearGridState(token); return; } modalOpen.value = true; storId.value = ''; - await clearGridState(); + await clearGridState(token); if (token !== openToken || !modalOpen.value) return; const data = modalApi.getData<{ storId: string }>(); if (!data?.storId) { From e5f56057c3241b2d17564df5ea2753dc7af85a09 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:37:44 +0800 Subject: [PATCH 18/24] =?UTF-8?q?feat:=20=E6=8C=89=E5=8F=82=E8=80=83?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E5=AE=9E=E7=8E=B0=E5=87=BA=E5=BA=93=E5=8D=95?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/wms/iostorinv/index.ts | 2 + .../wms/iostorinv/modules/detail-builders.ts | 1 + .../src/views/wms/iostorinv/modules/form.vue | 323 ++++++++++++++++-- .../iostorinv/modules/outbound-form.test.ts | 78 +++++ .../wms/iostorinv/modules/outbound-form.ts | 67 ++++ 5 files changed, 445 insertions(+), 26 deletions(-) create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts create mode 100644 nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts 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 197dc02b..30bff1d0 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 @@ -51,7 +51,9 @@ export namespace WmsIostorInvApi { /** 出库明细页面展示模型(提交时应转换为 OutboundDetail) */ export interface OutboundDisplayDetail extends OutboundDetail { + groupId?: number | string; materialName?: string; + rowKey?: string; sapBatchNo?: string; vehicleCode?: string; } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts index df077843..0d3556be 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-builders.ts @@ -11,6 +11,7 @@ export function mapExpandedInventory( inventory: WmsIostorInvApi.AvailableInventory[], ): WmsIostorInvApi.OutboundDisplayDetail[] { return inventory.map((row) => ({ + groupId: row.groupId, materialCode: row.materialCode, materialId: row.materialId, materialName: row.materialName ?? undefined, diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue index dc52fb96..2261c764 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue @@ -4,28 +4,48 @@ import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; import { computed, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; +import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; -import { message } from 'antdv-next'; +import { Button, Input, InputNumber, message, Space, Table } from 'antdv-next'; +import dayjs from 'dayjs'; import { useVbenForm } from '#/adapter/form'; -import { createIostorInv, getIostorInv, updateIostorInv } from '#/api/wms/iostorinv'; +import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr'; +import { + createOutbound, + getIostorInv, + updateIostorInv, +} from '#/api/wms/iostorinv'; import { $t } from '#/locales'; import { useFormSchema } from '../data'; +import { summarizeDetails } from './detail-summary'; +import InventorySelectComponent from './inventory-select.vue'; +import ManualDetailComponent from './manual-detail.vue'; +import { + buildOutboundPayload, + mergeOutboundInventory, + type OutboundFormDetail, + shouldClearDetailsForWarehouseChange, +} from './outbound-form'; const emit = defineEmits(['success']); const formData = ref(); -const getTitle = computed(() => { - return formData.value?.iostorinvId - ? $t('ui.actionTitle.edit', ['出入库单主表']) - : $t('ui.actionTitle.create', ['出入库单主表']); -}); +const details = ref([]); +const currentStorId = ref(); +let manualRowSequence = 0; -const [Form, formApi] = useVbenForm({ +const isEdit = computed(() => Boolean(formData.value?.iostorinvId)); +const getTitle = computed(() => + isEdit.value + ? $t('ui.actionTitle.edit', ['出入库单主表']) + : '出库单新增', +); + +const [LegacyForm, legacyFormApi] = useVbenForm({ commonConfig: { - componentProps: { - class: 'w-full', - }, + componentProps: { class: 'w-full' }, formItemClass: 'col-span-2', labelWidth: 80, }, @@ -34,18 +54,218 @@ const [Form, formApi] = useVbenForm({ showDefaultActions: false, }); +async function handleWarehouseChange(nextStorId?: string) { + const shouldClear = shouldClearDetailsForWarehouseChange( + currentStorId.value, + nextStorId, + ); + currentStorId.value = nextStorId; + if (shouldClear && details.value.length > 0) { + details.value = []; + await refreshSummary(); + } +} + +const [OutboundForm, outboundFormApi] = useVbenForm({ + commonConfig: { + componentProps: { class: 'w-full' }, + formItemClass: 'col-span-1', + labelWidth: 88, + }, + handleValuesChange: (values) => { + if (Object.hasOwn(values, 'storId')) { + void handleWarehouseChange(values.storId); + } + }, + layout: 'horizontal', + schema: [ + { + component: 'Input', + componentProps: { disabled: true }, + defaultValue: '保存时自动生成', + fieldName: 'billCode', + label: '单据号', + }, + { + component: 'ApiSelect', + componentProps: { + allowClear: true, + api: getBsrealStorAttrSimpleList, + labelField: 'storName', + placeholder: '请选择仓库', + valueField: 'storId', + }, + fieldName: 'storId', + label: '仓库', + rules: 'required', + }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE, 'string'), + placeholder: '请选择业务类型', + }, + fieldName: 'billType', + label: '业务类型', + rules: 'required', + }, + { + component: 'Input', + componentProps: { disabled: true }, + defaultValue: '生成', + fieldName: 'billStatus', + label: '单据状态', + }, + { + component: 'InputNumber', + componentProps: { disabled: true, min: 0 }, + defaultValue: 0, + fieldName: 'detailCount', + label: '明细数', + }, + { + component: 'Input', + componentProps: { disabled: true }, + defaultValue: '0.000', + fieldName: 'totalWeight', + label: '总重量', + }, + { + component: 'DatePicker', + componentProps: { class: 'w-full', format: 'YYYY-MM-DD' }, + defaultValue: dayjs(), + fieldName: 'bizDate', + label: '业务日期', + rules: 'required', + }, + { + component: 'Textarea', + componentProps: { allowClear: true, placeholder: '请输入备注', rows: 1 }, + fieldName: 'remark', + label: '备注', + }, + ], + showDefaultActions: false, + wrapperClass: 'grid grid-cols-1 gap-x-4 md:grid-cols-2 xl:grid-cols-4', +}); + +const [InventorySelect, inventorySelectApi] = useVbenModal({ + connectedComponent: InventorySelectComponent, + destroyOnClose: true, +}); +const [ManualDetail, manualDetailApi] = useVbenModal({ + connectedComponent: ManualDetailComponent, + destroyOnClose: true, +}); + +const columns = [ + { key: 'index', title: '序号', width: 60 }, + { dataIndex: 'materialCode', key: 'materialCode', title: '物料编码', width: 130 }, + { dataIndex: 'materialName', key: 'materialName', title: '物料名称', width: 150 }, + { dataIndex: 'vehicleCode', key: 'vehicleCode', title: '箱号', width: 130 }, + { dataIndex: 'pcsn', key: 'pcsn', title: '子卷号', width: 130 }, + { dataIndex: 'sapBatchNo', key: 'sapBatchNo', title: 'SAP批次号', width: 130 }, + { dataIndex: 'planQty', key: 'planQty', title: '出库重量', width: 130 }, + { dataIndex: 'qtyUnitName', key: 'qtyUnitName', title: '单位', width: 80 }, + { dataIndex: 'sourceBillCode', key: 'sourceBillCode', title: '源单号', width: 140 }, + { dataIndex: 'remark', key: 'remark', title: '明细备注', width: 180 }, + { key: 'action', title: '操作', width: 80, fixed: 'right' as const }, +]; + +async function refreshSummary() { + const summary = summarizeDetails(details.value); + await outboundFormApi.setValues({ + detailCount: summary.detailCount, + totalWeight: summary.totalWeight.toFixed(3), + }); +} + +async function openInventorySelect() { + const values = await outboundFormApi.getValues(); + if (!values.storId) { + message.warning('请先选择仓库'); + return; + } + inventorySelectApi.setData({ storId: values.storId }).open(); +} + +function openManualDetail() { + manualDetailApi.open(); +} + +async function handleInventorySelect(rows: WmsIostorInvApi.OutboundDisplayDetail[]) { + const keyedRows = rows.map((row) => ({ + ...row, + rowKey: `inventory-${String(row.groupId)}`, + })); + details.value = mergeOutboundInventory(details.value, keyedRows); + await refreshSummary(); +} + +async function handleManualSelect(rows: WmsIostorInvApi.OutboundDisplayDetail[]) { + details.value.push( + ...rows.map((row) => ({ + ...row, + rowKey: `manual-${Date.now()}-${++manualRowSequence}`, + })), + ); + await refreshSummary(); +} + +async function removeDetail(index: number) { + details.value.splice(index, 1); + await refreshSummary(); +} + +function validateDetails() { + if (details.value.length === 0) { + message.warning('请至少添加一条出库明细'); + return false; + } + const invalidIndex = details.value.findIndex( + (detail) => + !detail.materialCode || + !Number.isFinite(Number(detail.planQty)) || + Number(detail.planQty) <= 0, + ); + if (invalidIndex >= 0) { + message.warning(`第 ${invalidIndex + 1} 条明细物料编码不能为空,出库重量必须大于 0`); + return false; + } + return true; +} + const [Modal, modalApi] = useVbenModal({ async onConfirm() { - const { valid } = await formApi.validate(); - if (!valid) { + if (isEdit.value) { + const { valid } = await legacyFormApi.validate(); + if (!valid) return; + modalApi.lock(); + try { + await updateIostorInv( + (await legacyFormApi.getValues()) as WmsIostorInvApi.IostorInv, + ); + await modalApi.close(); + emit('success'); + message.success($t('ui.actionMessage.operationSuccess')); + } finally { + modalApi.unlock(); + } return; } + + const { valid } = await outboundFormApi.validate(); + if (!valid || !validateDetails()) return; + const values = await outboundFormApi.getValues(); modalApi.lock(); - // 提交表单 - const data = (await formApi.getValues()) as WmsIostorInvApi.IostorInv; try { - await (formData.value?.iostorinvId ? updateIostorInv(data) : createIostorInv(data)); - // 关闭并提示 + await createOutbound( + buildOutboundPayload( + { ...values, bizDate: dayjs(values.bizDate).valueOf() }, + details.value, + ), + ); await modalApi.close(); emit('success'); message.success($t('ui.actionMessage.operationSuccess')); @@ -53,21 +273,33 @@ const [Modal, modalApi] = useVbenModal({ modalApi.unlock(); } }, - async onOpenChange(isOpen: boolean) { - if (!isOpen) { + async onOpenChange(open) { + if (!open) { formData.value = undefined; + details.value = []; + currentStorId.value = undefined; return; } - // 加载数据 const data = modalApi.getData(); - if (!data || !data.iostorinvId) { + if (!data?.iostorinvId) { + formData.value = undefined; + details.value = []; + currentStorId.value = undefined; + await outboundFormApi.resetForm(); + await outboundFormApi.setValues({ + billCode: '保存时自动生成', + billStatus: '生成', + bizDate: dayjs(), + detailCount: 0, + totalWeight: '0.000', + }); return; } modalApi.lock(); try { - formData.value = await getIostorInv(data.iostorinvId); - // 设置到 values - await formApi.setValues(formData.value); + formData.value = data; + formData.value = await getIostorInv(Number(data.iostorinvId)); + await legacyFormApi.setValues(formData.value); } finally { modalApi.unlock(); } @@ -76,7 +308,46 @@ const [Modal, modalApi] = useVbenModal({ - - + + + + + + 出库明细 + + 选择库存 + 新增汇总 + + + + + {{ index + 1 }} + + + + + + + + 删除 + + + + + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts new file mode 100644 index 00000000..502f3e99 --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildOutboundPayload, + mergeOutboundInventory, + shouldClearDetailsForWarehouseChange, +} from './outbound-form'; + +describe('buildOutboundPayload', () => { + it('将业务日期转换成毫秒,并剥离所有仅展示字段和汇总字段', () => { + const bizDate = new Date('2026-07-22T08:30:00+08:00'); + const payload = buildOutboundPayload( + { billType: 'SALE', bizDate, remark: '主备注', storId: 'S-1' }, + [ + { + groupId: 11, + materialCode: 'M-1', + materialId: '1', + materialName: '物料一', + pcsn: 'P-1', + planQty: '12.5', + qtyUnitId: 'kg', + qtyUnitName: '千克', + remark: '明细备注', + rowKey: 'inventory-11', + sapBatchNo: 'SAP-1', + sourceBillCode: 'SRC-1', + sourceBillType: 'SALE', + sourceBilldtlId: 'D-1', + vehicleCode: 'BOX-1', + }, + ], + ); + + expect(payload.bizDate).toBe(bizDate.valueOf()); + expect(payload.details).toEqual([ + { + materialCode: 'M-1', + materialId: '1', + pcsn: 'P-1', + planQty: 12.5, + qtyUnitId: 'kg', + qtyUnitName: '千克', + remark: '明细备注', + sourceBillCode: 'SRC-1', + sourceBillType: 'SALE', + sourceBilldtlId: 'D-1', + }, + ]); + expect(payload).not.toHaveProperty('detailCount'); + expect(payload).not.toHaveProperty('totalWeight'); + }); +}); + +describe('mergeOutboundInventory', () => { + it('按 groupId 去除已添加库存和同批重复库存', () => { + const existing = [{ groupId: 1, materialCode: 'OLD', planQty: 1 }]; + const incoming = [ + { groupId: '1', materialCode: 'DUP', planQty: 2 }, + { groupId: 2, materialCode: 'NEW', planQty: 3 }, + { groupId: '2', materialCode: 'NEW-DUP', planQty: 4 }, + ]; + + expect(mergeOutboundInventory(existing, incoming)).toEqual([ + existing[0], + incoming[1], + ]); + }); +}); + +describe('shouldClearDetailsForWarehouseChange', () => { + it('初始化和同值回填不清空,实际变化才清空', () => { + expect(shouldClearDetailsForWarehouseChange(undefined, 'S-1')).toBe(false); + expect(shouldClearDetailsForWarehouseChange('S-1', 'S-1')).toBe(false); + expect(shouldClearDetailsForWarehouseChange('S-1', 'S-2')).toBe(true); + expect(shouldClearDetailsForWarehouseChange('S-1', undefined)).toBe(true); + }); +}); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts new file mode 100644 index 00000000..a47ba6ea --- /dev/null +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts @@ -0,0 +1,67 @@ +import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; + +export interface OutboundFormValues { + billType?: string; + bizDate?: number | string | { valueOf: () => number }; + remark?: string; + storId?: string; +} + +export interface OutboundFormDetail + extends Omit { + groupId?: number | string; + planQty: number | string; + rowKey?: string; +} + +export function buildOutboundPayload( + values: OutboundFormValues, + details: OutboundFormDetail[], +): WmsIostorInvApi.OutboundCreateReq { + return { + billType: values.billType!, + bizDate: Number(values.bizDate?.valueOf()), + details: details.map((detail) => ({ + materialCode: detail.materialCode, + materialId: detail.materialId, + pcsn: detail.pcsn, + planQty: Number(detail.planQty), + qtyUnitId: detail.qtyUnitId, + qtyUnitName: detail.qtyUnitName, + remark: detail.remark, + sourceBillCode: detail.sourceBillCode, + sourceBillType: detail.sourceBillType, + sourceBilldtlId: detail.sourceBilldtlId, + })), + remark: values.remark || undefined, + storId: values.storId!, + }; +} + +/** 库存行跨多次选择时,仍按后端组盘记录标识去重。 */ +export function mergeOutboundInventory( + existing: T[], + incoming: T[], +) { + const result = [...existing]; + const groupIds = new Set( + existing + .map(({ groupId }) => groupId) + .filter((groupId) => groupId !== undefined) + .map(String), + ); + incoming.forEach((row) => { + if (row.groupId === undefined || !groupIds.has(String(row.groupId))) { + result.push(row); + if (row.groupId !== undefined) groupIds.add(String(row.groupId)); + } + }); + return result; +} + +export function shouldClearDetailsForWarehouseChange( + previousStorId?: string, + nextStorId?: string, +) { + return Boolean(previousStorId && previousStorId !== nextStorId); +} From 88b70e67296f06fbcbd808525365938f08022923 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:43:26 +0800 Subject: [PATCH 19/24] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=8D=95=E5=AD=97=E5=85=B8=E5=8A=A0=E8=BD=BD=E4=B8=8E?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/wms/iostorinv/index.ts | 4 +-- .../src/views/wms/iostorinv/modules/form.vue | 32 ++++++++++++++++--- .../iostorinv/modules/outbound-form.test.ts | 13 ++++++++ .../wms/iostorinv/modules/outbound-form.ts | 13 ++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) 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 30bff1d0..7896257a 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 @@ -110,9 +110,9 @@ export function getIostorInvPage(params: PageParam) { } /** 查询出入库单主表详情 */ -export function getIostorInv(id: number) { +export function getIostorInv(id: string) { return requestClient.get( - `/wms/iostor-inv/get?id=${id}`, + `/wms/iostor-inv/get?id=${encodeURIComponent(id)}`, ); } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue index 2261c764..b28a4926 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue @@ -4,13 +4,12 @@ import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; import { computed, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; -import { DICT_TYPE } from '@vben/constants'; -import { getDictOptions } from '@vben/hooks'; import { Button, Input, InputNumber, message, Space, Table } from 'antdv-next'; import dayjs from 'dayjs'; import { useVbenForm } from '#/adapter/form'; +import { getSimpleDictDataList } from '#/api/system/dict/data'; import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr'; import { createOutbound, @@ -25,6 +24,7 @@ import InventorySelectComponent from './inventory-select.vue'; import ManualDetailComponent from './manual-detail.vue'; import { buildOutboundPayload, + filterOutboundBillTypeOptions, mergeOutboundInventory, type OutboundFormDetail, shouldClearDetailsForWarehouseChange, @@ -35,6 +35,7 @@ const formData = ref(); const details = ref([]); const currentStorId = ref(); let manualRowSequence = 0; +let billTypeRequest: null | ReturnType = null; const isEdit = computed(() => Boolean(formData.value?.iostorinvId)); const getTitle = computed(() => @@ -103,7 +104,7 @@ const [OutboundForm, outboundFormApi] = useVbenForm({ component: 'Select', componentProps: { allowClear: true, - options: getDictOptions(DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE, 'string'), + options: [], placeholder: '请选择业务类型', }, fieldName: 'billType', @@ -159,6 +160,28 @@ const [ManualDetail, manualDetailApi] = useVbenModal({ destroyOnClose: true, }); +async function loadBillTypeOptions() { + billTypeRequest ??= getSimpleDictDataList(); + const request = billTypeRequest; + try { + const dictItems = await request; + outboundFormApi.updateSchema([ + { + componentProps: { + allowClear: true, + options: filterOutboundBillTypeOptions(dictItems), + placeholder: '请选择业务类型', + }, + fieldName: 'billType', + }, + ]); + } catch { + message.error('出库业务类型加载失败,请稍后重试'); + } finally { + if (billTypeRequest === request) billTypeRequest = null; + } +} + const columns = [ { key: 'index', title: '序号', width: 60 }, { dataIndex: 'materialCode', key: 'materialCode', title: '物料编码', width: 130 }, @@ -293,12 +316,13 @@ const [Modal, modalApi] = useVbenModal({ detailCount: 0, totalWeight: '0.000', }); + await loadBillTypeOptions(); return; } modalApi.lock(); try { formData.value = data; - formData.value = await getIostorInv(Number(data.iostorinvId)); + formData.value = await getIostorInv(data.iostorinvId); await legacyFormApi.setValues(formData.value); } finally { modalApi.unlock(); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts index 502f3e99..9f11026e 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts @@ -2,10 +2,23 @@ import { describe, expect, it } from 'vitest'; import { buildOutboundPayload, + filterOutboundBillTypeOptions, mergeOutboundInventory, shouldClearDetailsForWarehouseChange, } from './outbound-form'; +describe('filterOutboundBillTypeOptions', () => { + it('从冷缓存接口结果中只提取启用的出库业务类型', () => { + expect( + filterOutboundBillTypeOptions([ + { dictType: 'in_bill_type', label: '采购入库', status: 0, value: '100' }, + { dictType: 'out_bill_type', label: '销售出库', status: 0, value: '201' }, + { dictType: 'out_bill_type', label: '停用出库', status: 1, value: '299' }, + ]), + ).toEqual([{ label: '销售出库', value: '201' }]); + }); +}); + describe('buildOutboundPayload', () => { it('将业务日期转换成毫秒,并剥离所有仅展示字段和汇总字段', () => { const bizDate = new Date('2026-07-22T08:30:00+08:00'); diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts index a47ba6ea..4b95be41 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts @@ -1,5 +1,12 @@ import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; +export interface OutboundDictItem { + dictType: string; + label: string; + status?: number; + value: string; +} + export interface OutboundFormValues { billType?: string; bizDate?: number | string | { valueOf: () => number }; @@ -14,6 +21,12 @@ export interface OutboundFormDetail rowKey?: string; } +export function filterOutboundBillTypeOptions(items: OutboundDictItem[]) { + return items + .filter((item) => item.dictType === 'out_bill_type' && item.status !== 1) + .map(({ label, value }) => ({ label, value })); +} + export function buildOutboundPayload( values: OutboundFormValues, details: OutboundFormDetail[], From a84111880523ab39f6280324e7933ccc91294bd8 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:48:08 +0800 Subject: [PATCH 20/24] =?UTF-8?q?fix:=20=E9=98=B2=E6=AD=A2=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E7=BC=96=E8=BE=91=E8=AF=B7=E6=B1=82=E4=B8=B2=E6=89=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/wms/iostorinv/modules/form.vue | 48 +++++++++++++++++-- .../iostorinv/modules/outbound-form.test.ts | 11 +++++ .../wms/iostorinv/modules/outbound-form.ts | 15 ++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue index b28a4926..4eed162d 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue @@ -27,6 +27,7 @@ import { filterOutboundBillTypeOptions, mergeOutboundInventory, type OutboundFormDetail, + shouldApplyEditResult, shouldClearDetailsForWarehouseChange, } from './outbound-form'; @@ -36,6 +37,9 @@ const details = ref([]); const currentStorId = ref(); let manualRowSequence = 0; let billTypeRequest: null | ReturnType = null; +let mainModalOpen = false; +let mainOpenToken = 0; +let activeEditId: string | undefined; const isEdit = computed(() => Boolean(formData.value?.iostorinvId)); const getTitle = computed(() => @@ -297,18 +301,25 @@ const [Modal, modalApi] = useVbenModal({ } }, async onOpenChange(open) { + const token = ++mainOpenToken; + mainModalOpen = open; if (!open) { + activeEditId = undefined; formData.value = undefined; details.value = []; currentStorId.value = undefined; return; } + // 新会话不继承上一编辑详情请求留下的锁状态。 + modalApi.unlock(); const data = modalApi.getData(); if (!data?.iostorinvId) { + activeEditId = undefined; formData.value = undefined; details.value = []; currentStorId.value = undefined; await outboundFormApi.resetForm(); + if (!mainModalOpen || mainOpenToken !== token || activeEditId) return; await outboundFormApi.setValues({ billCode: '保存时自动生成', billStatus: '生成', @@ -316,16 +327,41 @@ const [Modal, modalApi] = useVbenModal({ detailCount: 0, totalWeight: '0.000', }); + if (!mainModalOpen || mainOpenToken !== token || activeEditId) return; await loadBillTypeOptions(); return; } + const editId = data.iostorinvId; + activeEditId = editId; + formData.value = data; modalApi.lock(); try { - formData.value = data; - formData.value = await getIostorInv(data.iostorinvId); - await legacyFormApi.setValues(formData.value); + const editResult = await getIostorInv(editId); + if ( + !shouldApplyEditResult( + mainModalOpen, + mainOpenToken, + token, + activeEditId, + editId, + ) + ) { + return; + } + formData.value = editResult; + await legacyFormApi.setValues(editResult); } finally { - modalApi.unlock(); + if ( + shouldApplyEditResult( + mainModalOpen, + mainOpenToken, + token, + activeEditId, + editId, + ) + ) { + modalApi.unlock(); + } } }, }); @@ -358,7 +394,9 @@ const [Modal, modalApi] = useVbenModal({ diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts index 9f11026e..fd71feea 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts @@ -4,9 +4,20 @@ import { buildOutboundPayload, filterOutboundBillTypeOptions, mergeOutboundInventory, + shouldApplyEditResult, shouldClearDetailsForWarehouseChange, } from './outbound-form'; +describe('shouldApplyEditResult', () => { + it('只允许当前打开会话中同一编辑单据的响应落表', () => { + expect(shouldApplyEditResult(true, 2, 2, 'B', 'B')).toBe(true); + expect(shouldApplyEditResult(true, 2, 1, 'B', 'A')).toBe(false); + expect(shouldApplyEditResult(true, 2, 2, 'B', 'A')).toBe(false); + expect(shouldApplyEditResult(true, 2, 1, undefined, 'A')).toBe(false); + expect(shouldApplyEditResult(false, 2, 2, 'B', 'B')).toBe(false); + }); +}); + describe('filterOutboundBillTypeOptions', () => { it('从冷缓存接口结果中只提取启用的出库业务类型', () => { expect( diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts index 4b95be41..4568d31c 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts @@ -7,6 +7,21 @@ export interface OutboundDictItem { value: string; } +export function shouldApplyEditResult( + modalOpen: boolean, + currentToken: number, + requestToken: number, + activeEditId?: string, + requestEditId?: string, +) { + return ( + modalOpen && + currentToken === requestToken && + Boolean(requestEditId) && + activeEditId === requestEditId + ); +} + export interface OutboundFormValues { billType?: string; bizDate?: number | string | { valueOf: () => number }; From a65f96dab6ff910dc452d7f1fd5d43b53768b073 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:51:36 +0800 Subject: [PATCH 21/24] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E6=B5=8B=E8=AF=95=20MyBatis=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java | 3 ++- .../wms/service/iostorinv/IostorInvServiceLocalSpringTest.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java index fc783563..5a4cf162 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java @@ -6,6 +6,7 @@ import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRes import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO; import cn.code.nl.module.wms.service.iostorinv.IostorInvServiceImpl; import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; @@ -95,7 +96,7 @@ class IostorinvDtlMapperTest { factory.setDataSource(dataSource); factory.setMapperLocations(new org.springframework.core.io.ClassPathResource( "mapper/iostorinvdtl/IostorinvDtlMapper.xml")); - org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); + MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); factory.setConfiguration(configuration); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index 3d68e44f..d7c64a8f 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -5,6 +5,7 @@ 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.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; +import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import jakarta.annotation.Resource; import org.aspectj.lang.ProceedingJoinPoint; @@ -204,7 +205,7 @@ class IostorInvServiceLocalSpringTest { SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); - org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); + MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); factory.setConfiguration(configuration); return factory.getObject(); From 4e276c5b05708c63a2bb8de065d4325cd29a02bd Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 16:02:43 +0800 Subject: [PATCH 22/24] =?UTF-8?q?fix:=20=E4=BF=9D=E5=AD=98=E5=89=8D?= =?UTF-8?q?=E9=87=8D=E6=A0=A1=E9=AA=8C=E5=87=BA=E5=BA=93=E5=BA=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/wms/enums/ErrorCodeConstants.java | 2 + .../iostorinv/vo/IostorInvCreateReqVO.java | 6 ++ .../iostorinvdtl/IostorinvDtlMapper.java | 4 ++ .../iostorinv/IostorInvServiceImpl.java | 50 +++++++++++++++++ .../iostorinvdtl/IostorinvDtlMapper.xml | 29 +++++++++- .../iostorinvdtl/IostorinvDtlMapperTest.java | 5 ++ .../IostorInvServiceLocalSpringTest.java | 55 +++++++++++++++++++ .../sql/iostorinv/available-inventory.sql | 2 + .../sql/iostorinv/create-outbound.sql | 13 +++++ .../src/api/wms/iostorinv/index.ts | 4 +- .../iostorinv/modules/outbound-form.test.ts | 12 ++++ .../wms/iostorinv/modules/outbound-form.ts | 2 + 12 files changed, 179 insertions(+), 5 deletions(-) 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 1e263ad5..ba77be31 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 @@ -14,6 +14,8 @@ public interface ErrorCodeConstants { ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(6_000_4, "单据号生成失败"); + ErrorCode IOSTOR_INV_INVENTORY_INVALID = new ErrorCode(6_000_5, "出库库存已变化,请刷新后重新选择完整箱库存"); + ErrorCode IOSTORINV_DTL_NOT_EXISTS = new ErrorCode(5, "出入库单明细不存在"); ErrorCode IOSTORINV_DIS_NOT_EXISTS = new ErrorCode(6, "出入库单分配不存在"); diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java index be8299c1..2b5faf60 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java @@ -39,6 +39,12 @@ public class IostorInvCreateReqVO { @Data public static class Detail { + @Schema(description = "组盘记录标识(仅库存选取方式提交)") + private Long groupId; + + @Schema(description = "箱号(仅库存选取方式提交)") + private String vehicleCode; + @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "物料编码不能为空") private String materialCode; diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java index e4bdde7d..629e5e1b 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java @@ -43,6 +43,10 @@ public interface IostorinvDtlMapper extends BaseMapperX { List selectAvailableInventoryByVehicleCodes( @Param("storId") String storId, @Param("vehicleCodes") Collection vehicleCodes); + /** 保存前重查并锁定指定仓库、箱号下的全部可用组盘行。 */ + List selectAvailableInventoryByVehicleCodesForUpdate( + @Param("storId") String storId, @Param("vehicleCodes") Collection vehicleCodes); + default PageResult selectPage(IostorinvDtlPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(IostorinvDtlDO::getIostorinvId, reqVO.getIostorinvId()) 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 60ec58d1..028633bc 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 @@ -54,6 +54,8 @@ public class IostorInvServiceImpl implements IostorInvService { @Override @Transactional(rollbackFor = Exception.class) public String createOutbound(IostorInvCreateReqVO reqVO) { + validateAndLockInventory(reqVO); + String ruleCode = "IO_CODE"; CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO(); codeGenerateReqDTO.setRuleCode(ruleCode); @@ -110,6 +112,54 @@ public class IostorInvServiceImpl implements IostorInvService { return iostorInv.getIostorinvId(); } + private void validateAndLockInventory(IostorInvCreateReqVO reqVO) { + List inventoryDetails = new ArrayList<>(); + Set requestedGroupIds = new HashSet<>(); + Set vehicleCodes = new LinkedHashSet<>(); + for (IostorInvCreateReqVO.Detail detail : reqVO.getDetails()) { + boolean hasGroupId = detail.getGroupId() != null; + boolean hasVehicleCode = StrUtil.isNotBlank(detail.getVehicleCode()); + if (hasGroupId != hasVehicleCode) { + throw exception(IOSTOR_INV_INVENTORY_INVALID); + } + if (!hasGroupId) { + continue; + } + if (!requestedGroupIds.add(detail.getGroupId())) { + throw exception(IOSTOR_INV_INVENTORY_INVALID); + } + inventoryDetails.add(detail); + vehicleCodes.add(detail.getVehicleCode()); + } + if (inventoryDetails.isEmpty()) { + return; + } + + List lockedRows = + iostorinvDtlMapper.selectAvailableInventoryByVehicleCodesForUpdate(reqVO.getStorId(), vehicleCodes); + Map lockedByGroupId = new HashMap<>(); + for (AvailableInventoryRespVO row : lockedRows) { + if (lockedByGroupId.put(row.getGroupId(), row) != null) { + throw exception(IOSTOR_INV_INVENTORY_INVALID); + } + } + if (!lockedByGroupId.keySet().equals(requestedGroupIds)) { + throw exception(IOSTOR_INV_INVENTORY_INVALID); + } + for (IostorInvCreateReqVO.Detail detail : inventoryDetails) { + AvailableInventoryRespVO row = lockedByGroupId.get(detail.getGroupId()); + if (!Objects.equals(detail.getVehicleCode(), row.getVehicleCode()) + || !Objects.equals(detail.getMaterialId(), row.getMaterialId()) + || !Objects.equals(detail.getMaterialCode(), row.getMaterialCode()) + || !Objects.equals(detail.getPcsn(), row.getPcsn()) + || detail.getPlanQty() == null || detail.getPlanQty().signum() <= 0 + || detail.getPlanQty().compareTo(row.getAvailableQty()) > 0) { + throw exception(IOSTOR_INV_INVENTORY_INVALID); + } + } + // 建单仅验证库存快照;库存占用和冻结数量由后续分配流程处理。 + } + @Override public PageResult getAvailableInventoryPage(AvailableInventoryPageReqVO reqVO) { return iostorinvDtlMapper.selectAvailableInventoryPage(reqVO); diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml index 1612604d..a07c69b6 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml @@ -9,7 +9,7 @@ gp.material_id AS material_id, gp.material_code AS material_code, mb.material_name AS material_name, - gp.qty - gp.frozen_qty AS available_qty, + gp.qty - COALESCE(gp.frozen_qty, 0) AS available_qty, gp.qty_unit_id AS qty_unit_id, gp.qty_unit_name AS qty_unit_name, gp.ext_code AS ext_code, @@ -33,7 +33,7 @@ LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 WHERE sa.stor_id = #{reqVO.storId} AND gp.status = '可用' - AND gp.qty - gp.frozen_qty > 0 + AND gp.qty - COALESCE(gp.frozen_qty, 0) > 0 AND gp.deleted = 0 AND gp.material_code LIKE CONCAT('%', #{reqVO.materialCode}, '%') @@ -60,7 +60,7 @@ LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 WHERE sa.stor_id = #{storId} AND gp.status = '可用' - AND gp.qty - gp.frozen_qty > 0 + AND gp.qty - COALESCE(gp.frozen_qty, 0) > 0 AND gp.deleted = 0 AND gp.vehicle_code IN @@ -69,4 +69,27 @@ ORDER BY gp.vehicle_code, gp.group_id + + SELECT + FROM wms_group_plate gp + INNER JOIN ( + SELECT stor_id, storagevehicle_code, MAX(stor_code) AS stor_code, MAX(stor_name) AS stor_name + FROM wms_structattr + WHERE deleted = 0 + GROUP BY stor_id, storagevehicle_code + ) sa ON sa.storagevehicle_code = gp.vehicle_code + LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.deleted = 0 + WHERE sa.stor_id = #{storId} + AND gp.status = '可用' + AND gp.qty - COALESCE(gp.frozen_qty, 0) > 0 + AND gp.deleted = 0 + AND gp.vehicle_code IN + + #{vehicleCode} + + ORDER BY gp.vehicle_code, gp.group_id + FOR UPDATE + + diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java index 5a4cf162..f5be587a 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapperTest.java @@ -58,6 +58,11 @@ class IostorinvDtlMapperTest { assertTrue(expanded.stream().allMatch(item -> "STOR-001".equals(item.getStorId()))); assertEquals(2, expanded.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count()); assertTrue(expanded.stream().allMatch(item -> item.getAvailableQty().signum() > 0)); + assertEquals(0, new java.math.BigDecimal("5").compareTo(expanded.get(1).getAvailableQty())); + + List locked = mapper.selectAvailableInventoryByVehicleCodesForUpdate( + "STOR-001", List.of("BOX-001")); + assertEquals(List.of(1L, 2L), locked.stream().map(AvailableInventoryRespVO::getGroupId).toList()); AvailableInventoryPageReqVO warehousePageReqVO = new AvailableInventoryPageReqVO(); warehousePageReqVO.setStorId("STOR-001"); diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index d7c64a8f..63ef240d 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -156,6 +156,57 @@ class IostorInvServiceLocalSpringTest { assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); } + @Test + void shouldRejectMissingChildOfSameBoxBeforeGeneratingCode() { + jdbcTemplate.update("INSERT INTO wms_group_plate VALUES (104,'BOX-01','可用','MID-01','MAT-01','PCSN-EXTRA',1,0,'KG','千克',NULL,NULL,NULL,FALSE)"); + assertInventoryRejected(buildRequest()); + } + + @Test + void shouldRejectCrossWarehouseGroup() { + IostorInvCreateReqVO request = buildRequest(); + IostorInvCreateReqVO.Detail detail = request.getDetails().get(0); + detail.setGroupId(103L); + detail.setVehicleCode("BOX-X"); + detail.setPcsn("PCSN-X"); + detail.setPlanQty(new BigDecimal("5")); + assertInventoryRejected(request); + } + + @Test + void shouldRejectQuantityAboveAvailable() { + IostorInvCreateReqVO request = buildRequest(); + request.getDetails().get(1).setPlanQty(new BigDecimal("7.251")); + assertInventoryRejected(request); + } + + @Test + void shouldRejectDuplicateGroup() { + IostorInvCreateReqVO request = buildRequest(); + request.getDetails().get(1).setGroupId(101L); + request.getDetails().get(1).setVehicleCode("BOX-01"); + assertInventoryRejected(request); + } + + @Test + void shouldRejectInventoryChangedToUnavailable() { + jdbcTemplate.update("UPDATE wms_group_plate SET status = '不可用' WHERE group_id = 101"); + assertInventoryRejected(buildRequest()); + } + + @Test + void shouldRejectInventoryChangedToFullyFrozen() { + jdbcTemplate.update("UPDATE wms_group_plate SET frozen_qty = qty WHERE group_id = 101"); + assertInventoryRejected(buildRequest()); + } + + private void assertInventoryRejected(IostorInvCreateReqVO request) { + ServiceException ex = assertThrows(ServiceException.class, () -> iostorInvService.createOutbound(request)); + assertEquals("出库库存已变化,请刷新后重新选择完整箱库存", ex.getMessage()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + private IostorInvCreateReqVO buildRequest() { IostorInvCreateReqVO request = new IostorInvCreateReqVO(); request.setBillType("销售出库"); @@ -179,6 +230,8 @@ class IostorInvServiceLocalSpringTest { detail.setSourceBillType("ORDER"); detail.setSourceBilldtlId("SRC-DTL-" + suffix); detail.setRemark(remark); + detail.setGroupId(Long.valueOf("1" + suffix)); + detail.setVehicleCode("BOX-" + suffix); return detail; } @@ -205,6 +258,8 @@ class IostorInvServiceLocalSpringTest { SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); + factory.setMapperLocations(new org.springframework.core.io.ClassPathResource( + "mapper/iostorinvdtl/IostorinvDtlMapper.xml")); MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); factory.setConfiguration(configuration); diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql index 3cd89ffc..cfa74a51 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/available-inventory.sql @@ -55,3 +55,5 @@ VALUES (1, 'BOX-001', '可用', '1001', 'MAT-001', 'PCSN-001', 10, 2, 'UNIT-1', (4, 'BOX-003', '可用', '1001', 'MAT-001', 'PCSN-004', 10, 10, 'UNIT-1', '卷', 'EXT-4', 'TYPE-1', 'DTL-4', FALSE), (5, 'BOX-004', '可用', '1001', 'MAT-001', 'PCSN-005', 10, 0, 'UNIT-1', '卷', 'EXT-5', 'TYPE-1', 'DTL-5', TRUE), (6, 'BOX-005', '可用', '1001', 'MAT-001', 'PCSN-006', 10, 0, 'UNIT-1', '卷', 'EXT-6', 'TYPE-1', 'DTL-6', FALSE); + +UPDATE wms_group_plate SET frozen_qty = NULL WHERE group_id = 2; diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql index d684f115..fa130195 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -1,5 +1,8 @@ DROP TABLE IF EXISTS wms_iostorinvdtl; DROP TABLE IF EXISTS wms_iostorinv; +DROP TABLE IF EXISTS wms_group_plate; +DROP TABLE IF EXISTS wms_structattr; +DROP TABLE IF EXISTS base_materialbase; CREATE TABLE wms_iostorinv ( iostorinv_id VARCHAR(64) PRIMARY KEY, bill_code VARCHAR(64), io_type VARCHAR(32), bill_type VARCHAR(64), biz_date TIMESTAMP, @@ -22,3 +25,13 @@ CREATE TABLE wms_iostorinvdtl ( material_id VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE ); + +CREATE TABLE base_materialbase (material_id VARCHAR(64) PRIMARY KEY, material_name VARCHAR(64), deleted BOOLEAN NOT NULL); +CREATE TABLE wms_structattr (struct_id VARCHAR(64) PRIMARY KEY, storagevehicle_code VARCHAR(64), stor_id VARCHAR(64), stor_code VARCHAR(64), stor_name VARCHAR(64), deleted BOOLEAN NOT NULL); +CREATE TABLE wms_group_plate (group_id BIGINT PRIMARY KEY, vehicle_code VARCHAR(64), status VARCHAR(16), material_id VARCHAR(64), material_code VARCHAR(64), pcsn VARCHAR(64), qty DECIMAL(18,3), frozen_qty DECIMAL(18,3), qty_unit_id VARCHAR(64), qty_unit_name VARCHAR(64), ext_code VARCHAR(64), ext_type VARCHAR(64), ext_dtl_code VARCHAR(64), deleted BOOLEAN NOT NULL); +INSERT INTO base_materialbase VALUES ('MID-01','物料一',FALSE), ('MID-02','物料二',FALSE); +INSERT INTO wms_structattr VALUES ('S1','BOX-01','STOR-01','S01','一号仓',FALSE), ('S2','BOX-02','STOR-01','S01','一号仓',FALSE), ('S3','BOX-X','STOR-02','S02','二号仓',FALSE); +INSERT INTO wms_group_plate VALUES + (101,'BOX-01','可用','MID-01','MAT-01','PCSN-01',12.500,NULL,'KG','千克',NULL,NULL,NULL,FALSE), + (102,'BOX-02','可用','MID-02','MAT-02','PCSN-02',8.000,0.750,'KG','千克',NULL,NULL,NULL,FALSE), + (103,'BOX-X','可用','MID-01','MAT-01','PCSN-X',5.000,0,'KG','千克',NULL,NULL,NULL,FALSE); 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 7896257a..d180c9b3 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 @@ -37,6 +37,8 @@ export namespace WmsIostorInvApi { /** 出库单可持久化明细 */ export interface OutboundDetail { + groupId?: number | string; // 组盘记录标识(库存选择方式) + vehicleCode?: string; // 箱号(库存选择方式) materialCode: string; // 物料编码 materialId?: string; // 物料标识 pcsn?: string; // 批次序列号 @@ -51,11 +53,9 @@ export namespace WmsIostorInvApi { /** 出库明细页面展示模型(提交时应转换为 OutboundDetail) */ export interface OutboundDisplayDetail extends OutboundDetail { - groupId?: number | string; materialName?: string; rowKey?: string; sapBatchNo?: string; - vehicleCode?: string; } /** 出库单新增请求 */ diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts index fd71feea..0726b1cb 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.test.ts @@ -59,6 +59,7 @@ describe('buildOutboundPayload', () => { expect(payload.bizDate).toBe(bizDate.valueOf()); expect(payload.details).toEqual([ { + groupId: 11, materialCode: 'M-1', materialId: '1', pcsn: 'P-1', @@ -69,11 +70,22 @@ describe('buildOutboundPayload', () => { sourceBillCode: 'SRC-1', sourceBillType: 'SALE', sourceBilldtlId: 'D-1', + vehicleCode: 'BOX-1', }, ]); expect(payload).not.toHaveProperty('detailCount'); expect(payload).not.toHaveProperty('totalWeight'); }); + + it('手工汇总行不附带库存引用字段', () => { + const payload = buildOutboundPayload( + { billType: 'SALE', bizDate: 1, storId: 'S-1' }, + [{ materialCode: 'M-1', planQty: 2 }], + ); + + expect(payload.details[0]?.groupId).toBeUndefined(); + expect(payload.details[0]?.vehicleCode).toBeUndefined(); + }); }); describe('mergeOutboundInventory', () => { diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts index 4568d31c..c9f5cfba 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/outbound-form.ts @@ -50,6 +50,7 @@ export function buildOutboundPayload( billType: values.billType!, bizDate: Number(values.bizDate?.valueOf()), details: details.map((detail) => ({ + groupId: detail.groupId, materialCode: detail.materialCode, materialId: detail.materialId, pcsn: detail.pcsn, @@ -60,6 +61,7 @@ export function buildOutboundPayload( sourceBillCode: detail.sourceBillCode, sourceBillType: detail.sourceBillType, sourceBilldtlId: detail.sourceBilldtlId, + vehicleCode: detail.vehicleCode, })), remark: values.remark || undefined, storId: values.storId!, From 390862b2dd9d9e163cff108422cd21dba04c90f2 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 16:05:37 +0800 Subject: [PATCH 23/24] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E9=94=81?= =?UTF-8?q?=E5=AE=9A=E5=BA=93=E5=AD=98=E5=BF=AB=E7=85=A7=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=87=BA=E5=BA=93=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iostorinv/IostorInvServiceImpl.java | 24 ++++++++++--------- .../IostorInvServiceLocalSpringTest.java | 14 ++++++++--- .../sql/iostorinv/create-outbound.sql | 4 ++-- 3 files changed, 26 insertions(+), 16 deletions(-) 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 028633bc..f77aac66 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 @@ -54,7 +54,7 @@ public class IostorInvServiceImpl implements IostorInvService { @Override @Transactional(rollbackFor = Exception.class) public String createOutbound(IostorInvCreateReqVO reqVO) { - validateAndLockInventory(reqVO); + Map inventorySnapshot = validateAndLockInventory(reqVO); String ruleCode = "IO_CODE"; CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO(); @@ -95,24 +95,25 @@ public class IostorInvServiceImpl implements IostorInvService { detailDO.setIostorinvdtlId(IdUtil.fastSimpleUUID()); detailDO.setIostorinvId(iostorInv.getIostorinvId()); detailDO.setSeqNo(index + 1); - detailDO.setMaterialCode(detail.getMaterialCode()); - detailDO.setMaterialId(detail.getMaterialId()); - detailDO.setPcsn(detail.getPcsn()); + AvailableInventoryRespVO inventory = inventorySnapshot.get(detail.getGroupId()); + detailDO.setMaterialCode(inventory == null ? detail.getMaterialCode() : inventory.getMaterialCode()); + detailDO.setMaterialId(inventory == null ? detail.getMaterialId() : inventory.getMaterialId()); + detailDO.setPcsn(inventory == null ? detail.getPcsn() : inventory.getPcsn()); detailDO.setPlanQty(detail.getPlanQty()); detailDO.setUnassignQty(detail.getPlanQty()); detailDO.setAssignQty(BigDecimal.ZERO); - detailDO.setQtyUnitId(detail.getQtyUnitId()); - detailDO.setQtyUnitName(detail.getQtyUnitName()); - detailDO.setSourceBillCode(detail.getSourceBillCode()); - detailDO.setSourceBillType(detail.getSourceBillType()); - detailDO.setSourceBilldtlId(detail.getSourceBilldtlId()); + detailDO.setQtyUnitId(inventory == null ? detail.getQtyUnitId() : inventory.getQtyUnitId()); + detailDO.setQtyUnitName(inventory == null ? detail.getQtyUnitName() : inventory.getQtyUnitName()); + detailDO.setSourceBillCode(inventory == null ? detail.getSourceBillCode() : inventory.getExtCode()); + detailDO.setSourceBillType(inventory == null ? detail.getSourceBillType() : inventory.getExtType()); + detailDO.setSourceBilldtlId(inventory == null ? detail.getSourceBilldtlId() : inventory.getExtDtlCode()); detailDO.setRemark(detail.getRemark()); iostorinvDtlMapper.insert(detailDO); } return iostorInv.getIostorinvId(); } - private void validateAndLockInventory(IostorInvCreateReqVO reqVO) { + private Map validateAndLockInventory(IostorInvCreateReqVO reqVO) { List inventoryDetails = new ArrayList<>(); Set requestedGroupIds = new HashSet<>(); Set vehicleCodes = new LinkedHashSet<>(); @@ -132,7 +133,7 @@ public class IostorInvServiceImpl implements IostorInvService { vehicleCodes.add(detail.getVehicleCode()); } if (inventoryDetails.isEmpty()) { - return; + return Collections.emptyMap(); } List lockedRows = @@ -158,6 +159,7 @@ public class IostorInvServiceImpl implements IostorInvService { } } // 建单仅验证库存快照;库存占用和冻结数量由后续分配流程处理。 + return lockedByGroupId; } @Override diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index 63ef240d..37f76725 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -63,7 +63,7 @@ class IostorInvServiceLocalSpringTest { } @Test - void shouldCreateOutboundHeaderAndDetailsInOneTransaction() { + void shouldCreateOutboundUsingLockedInventoryIdentityInsteadOfClientUnitAndSource() { String id = iostorInvService.createOutbound(buildRequest()); assertNotNull(id); @@ -101,10 +101,10 @@ class IostorInvServiceLocalSpringTest { assertEquals(id, details.get(1).headerId()); assertEquals(new DetailRow(details.get(0).detailId(), id, 1, "MAT-01", "MID-01", "PCSN-01", new BigDecimal("12.500"), BigDecimal.ZERO.setScale(3), new BigDecimal("12.500"), - "KG", "千克", "SRC-01", "ORDER", "SRC-DTL-01", "明细一"), details.get(0)); + "DB-KG", "数据库千克", "DB-SRC-01", "DB-TYPE", "DB-DTL-01", "明细一"), details.get(0)); assertEquals(new DetailRow(details.get(1).detailId(), id, 2, "MAT-02", "MID-02", "PCSN-02", new BigDecimal("7.250"), BigDecimal.ZERO.setScale(3), new BigDecimal("7.250"), - "KG", "千克", "SRC-02", "ORDER", "SRC-DTL-02", "明细二"), details.get(1)); + "DB-KG", "数据库千克", "DB-SRC-02", "DB-TYPE", "DB-DTL-02", "明细二"), details.get(1)); } @Test @@ -200,6 +200,14 @@ class IostorInvServiceLocalSpringTest { assertInventoryRejected(buildRequest()); } + @Test + void shouldRejectInventoryChangedToPartiallyFrozenBelowRequestedQuantity() { + jdbcTemplate.update("UPDATE wms_group_plate SET qty = 10, frozen_qty = 6 WHERE group_id = 101"); + IostorInvCreateReqVO request = buildRequest(); + request.getDetails().get(0).setPlanQty(new BigDecimal("5")); + assertInventoryRejected(request); + } + private void assertInventoryRejected(IostorInvCreateReqVO request) { ServiceException ex = assertThrows(ServiceException.class, () -> iostorInvService.createOutbound(request)); assertEquals("出库库存已变化,请刷新后重新选择完整箱库存", ex.getMessage()); diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql index fa130195..2d00a69e 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -32,6 +32,6 @@ CREATE TABLE wms_group_plate (group_id BIGINT PRIMARY KEY, vehicle_code VARCHAR( INSERT INTO base_materialbase VALUES ('MID-01','物料一',FALSE), ('MID-02','物料二',FALSE); INSERT INTO wms_structattr VALUES ('S1','BOX-01','STOR-01','S01','一号仓',FALSE), ('S2','BOX-02','STOR-01','S01','一号仓',FALSE), ('S3','BOX-X','STOR-02','S02','二号仓',FALSE); INSERT INTO wms_group_plate VALUES - (101,'BOX-01','可用','MID-01','MAT-01','PCSN-01',12.500,NULL,'KG','千克',NULL,NULL,NULL,FALSE), - (102,'BOX-02','可用','MID-02','MAT-02','PCSN-02',8.000,0.750,'KG','千克',NULL,NULL,NULL,FALSE), + (101,'BOX-01','可用','MID-01','MAT-01','PCSN-01',12.500,NULL,'DB-KG','数据库千克','DB-SRC-01','DB-TYPE','DB-DTL-01',FALSE), + (102,'BOX-02','可用','MID-02','MAT-02','PCSN-02',8.000,0.750,'DB-KG','数据库千克','DB-SRC-02','DB-TYPE','DB-DTL-02',FALSE), (103,'BOX-X','可用','MID-01','MAT-01','PCSN-X',5.000,0,'KG','千克',NULL,NULL,NULL,FALSE); From 05c3d92abf3541f019eb54933abd97e723087c5b Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Thu, 23 Jul 2026 13:28:17 +0800 Subject: [PATCH 24/24] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=87=BA=E5=BA=93?= =?UTF-8?q?=E5=8D=95=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/codegen/CodeSequenceMapper.xml | 3 +- .../admin/iostorinv/IostorInvController.java | 4 +- .../iostorinv/vo/IostorInvPageReqVO.java | 5 +- .../admin/iostorinv/vo/IostorInvRespVO.java | 8 + .../iostorinvdtl/IostorinvDtlController.java | 208 ++++++++--------- .../dal/dataobject/iostorinv/IostorInvDO.java | 4 +- .../iostorinvdtl/IostorinvDtlDO.java | 216 +++++++++--------- .../iostorinvdtl/IostorinvDtlMapper.java | 1 - .../service/iostorinv/IostorInvService.java | 4 +- .../iostorinv/IostorInvServiceImpl.java | 12 +- .../iostorinvdtl/IostorinvDtlService.java | 124 +++++----- .../iostorinvdtl/IostorinvDtlServiceImpl.java | 2 +- .../src/main/resources/application.yaml | 4 +- .../src/views/wms/iostorinv/data.ts | 45 +++- .../packages/constants/src/dict-enum.ts | 3 + 15 files changed, 335 insertions(+), 308 deletions(-) diff --git a/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml b/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml index 74909a0f..5d3572b8 100644 --- a/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml +++ b/nl-module-base/nl-module-base-server/src/main/resources/mapper/codegen/CodeSequenceMapper.xml @@ -14,7 +14,8 @@ VALUES (#{ruleId}, #{resetKey}, #{startAt} + #{step} - 1, #{maxValue}) ON DUPLICATE KEY UPDATE current_value = current_value + #{step}, - max_value = #{maxValue} + max_value = #{maxValue}, + deleted = 0 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 82f8c078..635fdf5f 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 @@ -41,7 +41,7 @@ public class IostorInvController { @PostMapping("/createOutbound") @Operation(summary = "创建出库单及明细") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") - public CommonResult createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) { + public CommonResult createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) { return success(iostorInvService.createOutbound(reqVO)); } @@ -64,7 +64,7 @@ public class IostorInvController { @PostMapping("/create") @Operation(summary = "创建出入库单主表") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") - public CommonResult createIostorInv(@Valid @RequestBody IostorInvSaveReqVO createReqVO) { + public CommonResult createIostorInv(@Valid @RequestBody IostorInvSaveReqVO createReqVO) { return success(iostorInvService.createIostorInv(createReqVO)); } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvPageReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvPageReqVO.java index 224ea155..ea47a533 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvPageReqVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvPageReqVO.java @@ -8,6 +8,7 @@ import java.math.BigDecimal; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; +import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 出入库单主表分页 Request VO") @@ -21,7 +22,7 @@ public class IostorInvPageReqVO extends PageParam { private String billType; @Schema(description = "业务日期") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDateTime[] bizDate; @Schema(description = "仓库标识", example = "12635") @@ -76,7 +77,7 @@ public class IostorInvPageReqVO extends PageParam { private String syscompanyid; @Schema(description = "是否已上传") - private String isUpload; + private Boolean isUpload; @Schema(description = "回传人", example = "25284") private String uploadOptid; diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvRespVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvRespVO.java index 35964cd1..737c5d38 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvRespVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvRespVO.java @@ -4,10 +4,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.math.BigDecimal; +import com.fasterxml.jackson.annotation.JsonFormat; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import cn.idev.excel.annotation.*; +import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + @Schema(description = "管理后台 - 出入库单主表 Response VO") @Data @ExcelIgnoreUnannotated @@ -23,12 +26,17 @@ public class IostorInvRespVO { @Schema(description = "业务日期", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("业务日期") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDateTime bizDate; @Schema(description = "仓库标识", example = "12635") @ExcelProperty("仓库标识") private String storId; + @Schema(description = "仓库", example = "1号仓") + @ExcelProperty("仓库") + private String storName; + @Schema(description = "来源方标识", example = "21190") @ExcelProperty("来源方标识") private String sourceId; diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinvdtl/IostorinvDtlController.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinvdtl/IostorinvDtlController.java index eef62d4b..dd2f69e2 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinvdtl/IostorinvDtlController.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinvdtl/IostorinvDtlController.java @@ -1,104 +1,104 @@ -package cn.code.nl.module.wms.controller.admin.iostorinvdtl; - -import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import org.springframework.validation.annotation.Validated; -import org.springframework.security.access.prepost.PreAuthorize; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Operation; - -import jakarta.validation.constraints.*; -import jakarta.validation.*; -import jakarta.servlet.http.*; -import java.util.*; -import java.io.IOException; - -import cn.code.nl.framework.common.pojo.PageParam; -import cn.code.nl.framework.common.pojo.PageResult; -import cn.code.nl.framework.common.pojo.CommonResult; -import cn.code.nl.framework.common.util.object.BeanUtils; -import static cn.code.nl.framework.common.pojo.CommonResult.success; - -import cn.code.nl.framework.excel.core.util.ExcelUtils; - -import cn.code.nl.framework.apilog.core.annotation.ApiAccessLog; -import static cn.code.nl.framework.apilog.core.enums.OperateTypeEnum.*; - -import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; -import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; -import cn.code.nl.module.wms.service.iostorinvdtl.IostorinvDtlService; - -@Tag(name = "管理后台 - 出入库单明细") -@RestController -@RequestMapping("/wms/iostorinv-dtl") -@Validated -public class IostorinvDtlController { - - @Resource - private IostorinvDtlService iostorinvDtlService; - - @PostMapping("/create") - @Operation(summary = "创建出入库单明细") - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:create')") - public CommonResult createIostorinvDtl(@Valid @RequestBody IostorinvDtlSaveReqVO createReqVO) { - return success(iostorinvDtlService.createIostorinvDtl(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新出入库单明细") - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:update')") - public CommonResult updateIostorinvDtl(@Valid @RequestBody IostorinvDtlSaveReqVO updateReqVO) { - iostorinvDtlService.updateIostorinvDtl(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除出入库单明细") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:delete')") - public CommonResult deleteIostorinvDtl(@RequestParam("id") String id) { - iostorinvDtlService.deleteIostorinvDtl(id); - return success(true); - } - - @DeleteMapping("/delete-list") - @Parameter(name = "ids", description = "编号", required = true) - @Operation(summary = "批量删除出入库单明细") - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:delete')") - public CommonResult deleteIostorinvDtlList(@RequestParam("ids") List ids) { - iostorinvDtlService.deleteIostorinvDtlListByIds(ids); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得出入库单明细") - @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:query')") - public CommonResult getIostorinvDtl(@RequestParam("id") String id) { - IostorinvDtlDO iostorinvDtl = iostorinvDtlService.getIostorinvDtl(id); - return success(BeanUtils.toBean(iostorinvDtl, IostorinvDtlRespVO.class)); - } - - @GetMapping("/page") - @Operation(summary = "获得出入库单明细分页") - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:query')") - public CommonResult> getIostorinvDtlPage(@Valid IostorinvDtlPageReqVO pageReqVO) { - PageResult pageResult = iostorinvDtlService.getIostorinvDtlPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IostorinvDtlRespVO.class)); - } - - @GetMapping("/export-excel") - @Operation(summary = "导出出入库单明细 Excel") - @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:export')") - @ApiAccessLog(operateType = EXPORT) - public void exportIostorinvDtlExcel(@Valid IostorinvDtlPageReqVO pageReqVO, - HttpServletResponse response) throws IOException { - pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); - List list = iostorinvDtlService.getIostorinvDtlPage(pageReqVO).getList(); - // 导出 Excel - ExcelUtils.write(response, "出入库单明细.xls", "数据", IostorinvDtlRespVO.class, - BeanUtils.toBean(list, IostorinvDtlRespVO.class)); - } - -} \ No newline at end of file +package cn.code.nl.module.wms.controller.admin.iostorinvdtl; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.code.nl.framework.common.pojo.PageParam; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.framework.common.util.object.BeanUtils; +import static cn.code.nl.framework.common.pojo.CommonResult.success; + +import cn.code.nl.framework.excel.core.util.ExcelUtils; + +import cn.code.nl.framework.apilog.core.annotation.ApiAccessLog; +import static cn.code.nl.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; +import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; +import cn.code.nl.module.wms.service.iostorinvdtl.IostorinvDtlService; + +@Tag(name = "管理后台 - 出入库单明细") +@RestController +@RequestMapping("/wms/iostorinv-dtl") +@Validated +public class IostorinvDtlController { + + @Resource + private IostorinvDtlService iostorinvDtlService; + + @PostMapping("/create") + @Operation(summary = "创建出入库单明细") + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:create')") + public CommonResult createIostorinvDtl(@Valid @RequestBody IostorinvDtlSaveReqVO createReqVO) { + return success(iostorinvDtlService.createIostorinvDtl(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新出入库单明细") + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:update')") + public CommonResult updateIostorinvDtl(@Valid @RequestBody IostorinvDtlSaveReqVO updateReqVO) { + iostorinvDtlService.updateIostorinvDtl(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除出入库单明细") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:delete')") + public CommonResult deleteIostorinvDtl(@RequestParam("id") String id) { + iostorinvDtlService.deleteIostorinvDtl(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除出入库单明细") + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:delete')") + public CommonResult deleteIostorinvDtlList(@RequestParam("ids") List ids) { + iostorinvDtlService.deleteIostorinvDtlListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得出入库单明细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:query')") + public CommonResult getIostorinvDtl(@RequestParam("id") String id) { + IostorinvDtlDO iostorinvDtl = iostorinvDtlService.getIostorinvDtl(id); + return success(BeanUtils.toBean(iostorinvDtl, IostorinvDtlRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得出入库单明细分页") + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:query')") + public CommonResult> getIostorinvDtlPage(@Valid IostorinvDtlPageReqVO pageReqVO) { + PageResult pageResult = iostorinvDtlService.getIostorinvDtlPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IostorinvDtlRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出出入库单明细 Excel") + @PreAuthorize("@ss.hasPermission('wms:iostorinv-dtl:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportIostorinvDtlExcel(@Valid IostorinvDtlPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = iostorinvDtlService.getIostorinvDtlPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "出入库单明细.xls", "数据", IostorinvDtlRespVO.class, + BeanUtils.toBean(list, IostorinvDtlRespVO.class)); + } + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinv/IostorInvDO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinv/IostorInvDO.java index 6035f4d9..939927ef 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinv/IostorInvDO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinv/IostorInvDO.java @@ -30,8 +30,8 @@ public class IostorInvDO extends BaseDO { /** * 出入单标识 */ - @TableId(type = IdType.INPUT) - private String iostorinvId; + @TableId + private Long iostorinvId; /** * 单据编号 */ diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinvdtl/IostorinvDtlDO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinvdtl/IostorinvDtlDO.java index 75c119da..5f65d7ab 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinvdtl/IostorinvDtlDO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/iostorinvdtl/IostorinvDtlDO.java @@ -1,110 +1,106 @@ -package cn.code.nl.module.wms.dal.dataobject.iostorinvdtl; - -import lombok.*; -import java.util.*; -import java.math.BigDecimal; -import java.math.BigDecimal; -import java.math.BigDecimal; -import java.math.BigDecimal; -import com.baomidou.mybatisplus.annotation.*; -import cn.code.nl.framework.mybatis.core.dataobject.BaseDO; - -/** - * 出入库单明细 DO - * - * @author 诺力管理员 - */ -@TableName("wms_iostorinvdtl") -@KeySequence("wms_iostorinvdtl_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IostorinvDtlDO extends BaseDO { - - /** - * 出入单明细标识 - */ - @TableId(type = IdType.INPUT) - private String iostorinvdtlId; - /** - * 出入单标识 - */ - private String iostorinvId; - /** - * 明细序号 - */ - private Integer seqNo; - /** - * 批次 - */ - private String pcsn; - /** - * 单据明细状态 - */ - private String billStatus; - /** - * 数量计量单位标识 - */ - private String qtyUnitId; - /** - * 数量计量单位名称 - */ - private String qtyUnitName; - /** - * 计划数量 - */ - private BigDecimal planQty; - /** - * 实际数量 - */ - private BigDecimal realQty; - /** - * 来源单据明细标识 - */ - private String sourceBilldtlId; - /** - * 来源单据类型 - */ - private String sourceBillType; - /** - * 来源单编号 - */ - private String sourceBillCode; - /** - * 来源单表名 - */ - private String sourceBillTable; - /** - * 备注 - */ - private String remark; - /** - * 已分配数量 - */ - private BigDecimal assignQty; - /** - * 未分配数量 - */ - private BigDecimal unassignQty; - /** - * 物料编号 - */ - private String materialCode; - /** - * 来源单指定上料口 - */ - private String sourceLoadPort; - /** - * 单据回传策略配置类名 - */ - private String callbackStrategy; - /** - * 物料标识 - */ - private String materialId; - - -} \ No newline at end of file +package cn.code.nl.module.wms.dal.dataobject.iostorinvdtl; + +import lombok.*; +import java.util.*; +import java.math.BigDecimal; +import java.math.BigDecimal; +import java.math.BigDecimal; +import java.math.BigDecimal; +import com.baomidou.mybatisplus.annotation.*; +import cn.code.nl.framework.mybatis.core.dataobject.BaseDO; + +/** + * 出入库单明细 DO + * + * @author 诺力管理员 + */ +@TableName("wms_iostorinvdtl") +@KeySequence("wms_iostorinvdtl_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IostorinvDtlDO { + + /** + * 出入单明细标识 + */ + @TableId + private Long iostorinvdtlId; + /** + * 出入单标识 + */ + private Long iostorinvId; + /** + * 明细序号 + */ + private Integer seqNo; + /** + * 批次 + */ + private String pcsn; + /** + * 单据明细状态 + */ + private String billStatus; + /** + * 数量计量单位标识 + */ + private String qtyUnitId; + + /** + * 计划数量 + */ + private BigDecimal planQty; + /** + * 实际数量 + */ + private BigDecimal realQty; + /** + * 来源单据明细标识 + */ + private String sourceBilldtlId; + /** + * 来源单据类型 + */ + private String sourceBillType; + /** + * 来源单编号 + */ + private String sourceBillCode; + /** + * 来源单表名 + */ + private String sourceBillTable; + /** + * 备注 + */ + private String remark; + /** + * 已分配数量 + */ + private BigDecimal assignQty; + /** + * 未分配数量 + */ + private BigDecimal unassignQty; + /** + * 物料编号 + */ + private String materialCode; + /** + * 来源单指定上料口 + */ + private String sourceLoadPort; + /** + * 单据回传策略配置类名 + */ + private String callbackStrategy; + /** + * 物料标识 + */ + private String materialId; + + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java index 629e5e1b..61324e0e 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java @@ -54,7 +54,6 @@ public interface IostorinvDtlMapper extends BaseMapperX { .eqIfPresent(IostorinvDtlDO::getPcsn, reqVO.getPcsn()) .eqIfPresent(IostorinvDtlDO::getBillStatus, reqVO.getBillStatus()) .eqIfPresent(IostorinvDtlDO::getQtyUnitId, reqVO.getQtyUnitId()) - .likeIfPresent(IostorinvDtlDO::getQtyUnitName, reqVO.getQtyUnitName()) .eqIfPresent(IostorinvDtlDO::getPlanQty, reqVO.getPlanQty()) .eqIfPresent(IostorinvDtlDO::getRealQty, reqVO.getRealQty()) .eqIfPresent(IostorinvDtlDO::getSourceBilldtlId, reqVO.getSourceBilldtlId()) 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 1b28f1e6..c2acd718 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 @@ -20,7 +20,7 @@ public interface IostorInvService { * @param reqVO 出库单创建信息 * @return 出库单编号 */ - String createOutbound(@Valid IostorInvCreateReqVO reqVO); + Long createOutbound(@Valid IostorInvCreateReqVO reqVO); /** * 获得指定仓库的可用库存分页。 @@ -44,7 +44,7 @@ public interface IostorInvService { * @param createReqVO 创建信息 * @return 编号 */ - String createIostorInv(@Valid IostorInvSaveReqVO createReqVO); + Long createIostorInv(@Valid IostorInvSaveReqVO createReqVO); /** * 更新出入库单主表 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 f77aac66..e25d6bd0 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 @@ -53,7 +53,7 @@ public class IostorInvServiceImpl implements IostorInvService { */ @Override @Transactional(rollbackFor = Exception.class) - public String createOutbound(IostorInvCreateReqVO reqVO) { + public Long createOutbound(IostorInvCreateReqVO reqVO) { Map inventorySnapshot = validateAndLockInventory(reqVO); String ruleCode = "IO_CODE"; @@ -77,13 +77,12 @@ public class IostorInvServiceImpl implements IostorInvService { .map(IostorInvCreateReqVO.Detail::getPlanQty) .reduce(BigDecimal.ZERO, BigDecimal::add); IostorInvDO iostorInv = new IostorInvDO(); - iostorInv.setIostorinvId(IdUtil.fastSimpleUUID()); iostorInv.setBillCode(codeResult.getData()); - iostorInv.setIoType("OUT"); + iostorInv.setIoType("1"); iostorInv.setBillType(reqVO.getBillType()); iostorInv.setBizDate(reqVO.getBizDate()); iostorInv.setStorId(reqVO.getStorId()); - iostorInv.setBillStatus("生成"); + iostorInv.setBillStatus("10"); iostorInv.setRemark(reqVO.getRemark()); iostorInv.setDetailCount(reqVO.getDetails().size()); iostorInv.setTotalWeight(totalWeight); @@ -92,7 +91,6 @@ public class IostorInvServiceImpl implements IostorInvService { for (int index = 0; index < reqVO.getDetails().size(); index++) { IostorInvCreateReqVO.Detail detail = reqVO.getDetails().get(index); IostorinvDtlDO detailDO = new IostorinvDtlDO(); - detailDO.setIostorinvdtlId(IdUtil.fastSimpleUUID()); detailDO.setIostorinvId(iostorInv.getIostorinvId()); detailDO.setSeqNo(index + 1); AvailableInventoryRespVO inventory = inventorySnapshot.get(detail.getGroupId()); @@ -102,8 +100,8 @@ public class IostorInvServiceImpl implements IostorInvService { detailDO.setPlanQty(detail.getPlanQty()); detailDO.setUnassignQty(detail.getPlanQty()); detailDO.setAssignQty(BigDecimal.ZERO); + detailDO.setBillStatus("10"); detailDO.setQtyUnitId(inventory == null ? detail.getQtyUnitId() : inventory.getQtyUnitId()); - detailDO.setQtyUnitName(inventory == null ? detail.getQtyUnitName() : inventory.getQtyUnitName()); detailDO.setSourceBillCode(inventory == null ? detail.getSourceBillCode() : inventory.getExtCode()); detailDO.setSourceBillType(inventory == null ? detail.getSourceBillType() : inventory.getExtType()); detailDO.setSourceBilldtlId(inventory == null ? detail.getSourceBilldtlId() : inventory.getExtDtlCode()); @@ -175,7 +173,7 @@ public class IostorInvServiceImpl implements IostorInvService { } @Override - public String createIostorInv(IostorInvSaveReqVO createReqVO) { + public Long createIostorInv(IostorInvSaveReqVO createReqVO) { // 插入 IostorInvDO iostorInv = BeanUtils.toBean(createReqVO, IostorInvDO.class); iostorInvMapper.insert(iostorInv); diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlService.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlService.java index e109f9ce..21fc23f0 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlService.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlService.java @@ -1,62 +1,62 @@ -package cn.code.nl.module.wms.service.iostorinvdtl; - -import java.util.*; -import jakarta.validation.*; -import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; -import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; -import cn.code.nl.framework.common.pojo.PageResult; -import cn.code.nl.framework.common.pojo.PageParam; - -/** - * 出入库单明细 Service 接口 - * - * @author 诺力管理员 - */ -public interface IostorinvDtlService { - - /** - * 创建出入库单明细 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - String createIostorinvDtl(@Valid IostorinvDtlSaveReqVO createReqVO); - - /** - * 更新出入库单明细 - * - * @param updateReqVO 更新信息 - */ - void updateIostorinvDtl(@Valid IostorinvDtlSaveReqVO updateReqVO); - - /** - * 删除出入库单明细 - * - * @param id 编号 - */ - void deleteIostorinvDtl(String id); - - /** - * 批量删除出入库单明细 - * - * @param ids 编号 - */ - void deleteIostorinvDtlListByIds(List ids); - - /** - * 获得出入库单明细 - * - * @param id 编号 - * @return 出入库单明细 - */ - IostorinvDtlDO getIostorinvDtl(String id); - - /** - * 获得出入库单明细分页 - * - * @param pageReqVO 分页查询 - * @return 出入库单明细分页 - */ - PageResult getIostorinvDtlPage(IostorinvDtlPageReqVO pageReqVO); - -} \ No newline at end of file +package cn.code.nl.module.wms.service.iostorinvdtl; + +import java.util.*; +import jakarta.validation.*; +import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; +import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; +import cn.code.nl.framework.common.pojo.PageResult; +import cn.code.nl.framework.common.pojo.PageParam; + +/** + * 出入库单明细 Service 接口 + * + * @author 诺力管理员 + */ +public interface IostorinvDtlService { + + /** + * 创建出入库单明细 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createIostorinvDtl(@Valid IostorinvDtlSaveReqVO createReqVO); + + /** + * 更新出入库单明细 + * + * @param updateReqVO 更新信息 + */ + void updateIostorinvDtl(@Valid IostorinvDtlSaveReqVO updateReqVO); + + /** + * 删除出入库单明细 + * + * @param id 编号 + */ + void deleteIostorinvDtl(String id); + + /** + * 批量删除出入库单明细 + * + * @param ids 编号 + */ + void deleteIostorinvDtlListByIds(List ids); + + /** + * 获得出入库单明细 + * + * @param id 编号 + * @return 出入库单明细 + */ + IostorinvDtlDO getIostorinvDtl(String id); + + /** + * 获得出入库单明细分页 + * + * @param pageReqVO 分页查询 + * @return 出入库单明细分页 + */ + PageResult getIostorinvDtlPage(IostorinvDtlPageReqVO pageReqVO); + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlServiceImpl.java index 6961e5f6..e50c722e 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlServiceImpl.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinvdtl/IostorinvDtlServiceImpl.java @@ -33,7 +33,7 @@ public class IostorinvDtlServiceImpl implements IostorinvDtlService { private IostorinvDtlMapper iostorinvDtlMapper; @Override - public String createIostorinvDtl(IostorinvDtlSaveReqVO createReqVO) { + public Long createIostorinvDtl(IostorinvDtlSaveReqVO createReqVO) { // 插入 IostorinvDtlDO iostorinvDtl = BeanUtils.toBean(createReqVO, IostorinvDtlDO.class); iostorinvDtlMapper.insert(iostorinvDtl); diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/application.yaml b/nl-module-wms/nl-module-wms-server/src/main/resources/application.yaml index e715a703..de00b69f 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/application.yaml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/application.yaml @@ -65,10 +65,10 @@ mybatis-plus: map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 global-config: db-config: - id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 + # id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 # id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 # id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 -# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) banner: false # 关闭控制台的 Banner 打印 diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts index 15e9a244..bd44aae6 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts @@ -3,6 +3,16 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; import { getRangePickerDefaultProps } from '#/utils'; +import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr'; +import {getDictOptions} from "@vben/hooks"; +import {DICT_TYPE} from "@vben/constants"; + + +// ========== 启用的仓库下拉 ========== +async function loadWarehouseList() { + const data = await getBsrealStorAttrSimpleList(); + return data || []; +} /** 新增/修改的表单 */ export function useFormSchema(): VbenFormSchema[] { @@ -260,7 +270,7 @@ export function useGridFormSchema(): VbenFormSchema[] { component: 'Select', componentProps: { allowClear: true, - options: [], + options: getDictOptions(DICT_TYPE.WMS_OUT_BILL_TYPE), placeholder: '请选择单据类型', }, }, @@ -275,11 +285,14 @@ export function useGridFormSchema(): VbenFormSchema[] { }, { fieldName: 'storId', - label: '仓库标识', - component: 'Input', + label: '仓库', + component: 'ApiSelect', componentProps: { + api: loadWarehouseList, + labelField: 'storName', + valueField: 'storId', + placeholder: '请选择仓库', allowClear: true, - placeholder: '请输入仓库标识', }, }, { @@ -288,7 +301,7 @@ export function useGridFormSchema(): VbenFormSchema[] { component: 'Select', componentProps: { allowClear: true, - options: [], + options: getDictOptions(DICT_TYPE.WMS_IO_BILL_STATUS), placeholder: '请选择单据状态', }, }, @@ -303,11 +316,12 @@ export function useGridFormSchema(): VbenFormSchema[] { }, { fieldName: 'isUpload', - label: '是否已回传', - component: 'Input', + label: '是否回传', + component: 'Select', componentProps: { allowClear: true, - placeholder: '请输入是否已回传', + options: getDictOptions(DICT_TYPE.COMMON_TRUE_FALSE, 'boolean'), + placeholder: '请选择是否回传', }, } ]; @@ -331,11 +345,11 @@ export function useGridColumns(): VxeTableGridOptions field: 'bizDate', title: '业务日期', minWidth: 120, - formatter: 'formatDateTime', + formatter: 'formatDate', }, { - field: 'storId', - title: '仓库标识', + field: 'storName', + title: '仓库', minWidth: 120, }, { @@ -372,6 +386,10 @@ export function useGridColumns(): VxeTableGridOptions field: 'billStatus', title: '单据状态', minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.WMS_IO_BILL_STATUS }, + }, }, { field: 'remark', @@ -382,6 +400,10 @@ export function useGridColumns(): VxeTableGridOptions field: 'createMode', title: '生成方式', minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.WMS_CREATE_TYPE }, + }, }, { field: 'createTime', @@ -444,4 +466,3 @@ export function useGridColumns(): VxeTableGridOptions }, ]; } - diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts index 14f2124e..0e79ff8a 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts @@ -287,6 +287,9 @@ const WMS_DICT = { WMS_GROUP_PLATE_STATUS: 'wms_group_plate_status', // 组盘记录 WMS_VEHICLE_TYPE: 'wms_vehicle_type', // 载具类型 WMS_OVER_STRUCT_TYPE: 'wms_over_struct_type', // 是否超限 + WMS_OUT_BILL_TYPE: 'wms_out_bill_type', // 出库单据类型 + WMS_IO_BILL_STATUS: 'wms_io_bill_status', // 出入库单据状态 + WMS_CREATE_TYPE: 'wms_create_type', // 生成方式 } as const; /** ========== TASK - 任务管理模块 ========== */