Files
huachuang/docs/superpowers/specs/2026-07-22-outbound-create-design.md

257 lines
8.4 KiB
Markdown
Raw Normal View History

# 出库单新增页面设计
## 概述
将出入库单主表(`iostorinv`)的新增弹窗从简单表单重构为出库单新增页面,包含主表表单 + 出库明细 table。参考原型图 `.image/出库新增.png`
### 范围
- 仅涉及**出库**场景(`ioType = "OUT"`
- 后端:新增 `createWithDetails` 接口,新增库存查询 Mapper SQL
- 前端:重构 `form.vue`,新增库存选择弹窗和手动新增汇总弹窗
- **不新增数据库字段**
---
## 主表字段映射
| 页面字段 | 数据来源/逻辑 | 对应 DO 字段 | 必填 | 可编辑 |
|---------|-------------|-------------|------|-------|
| 仓库 | `wms_bsrealstorattr` 启用列表 | `storId / storCode / storName` | 是 | 是 |
| 业务类型 | 字典 `out_bill_type` | `billType` | 是 | 是 |
| 单据号 | `CodeGenApi.generate("IO_CODE")` | `billCode` | 是 | 否(自动生成) |
| 业务日期 | 默认当天 | `bizDate` | 是 | 是 |
| 单据状态 | 固定"生成" | `billStatus` | 是 | 否 |
| 明细数 | `detailTable.length` | `detailCount` | 是 | 否(自动联动) |
| 总重量 | `sum(detail.planQty)` | `totalWeight` | 是 | 否(自动联动) |
| 备注 | 手动输入 | `remark` | 否 | 是 |
| 出入类型 | 固定 "OUT" | `ioType` | 是 | 否 |
---
## 出库明细字段映射(仅使用现有 DO 字段)
| 列 | 对应 DO 字段 | 数据来源 | 说明 |
|----|------------|---------|------|
| 序号 | `seqNo` | 自动编号 1,2,3... | |
| 箱号 | *(不存)* | `wms_group_plate.vehicle_code` | 仅页面展示,不入库 |
| 物料编码 | `materialCode` | 库存带入 / 手动选择 | |
| 物料名称 | *(不存)* | `MaterialBaseApi` 实时反查 | 仅页面展示 |
| 子卷号 | `pcsn` | `wms_group_plate.pcsn` 或手动输入 | 方式 B 时为空 |
| 出库重量 | `planQty` | 库存 `qty - frozenQty`(可编辑)或手动输入 | |
| 未分配数量 | `unassignQty` | 初始 = `planQty` | |
| 已分配数量 | `assignQty` | 初始 = 0 | |
| 单位 | `qtyUnitName` | 库存带入 / 物料默认单位 | |
| 批次 | `pcsn` | 同子卷号 | |
| 备注 | `remark` | 可选输入 | |
| 来源单据号 | `sourceBillCode` | `group_plate.extCode` | 库存带入时有值 |
| 来源单据类型 | `sourceBillType` | `group_plate.extType` | 库存带入时有值 |
| 来源单据明细 | `sourceBilldtlId` | `group_plate.extDtlCode` | 库存带入时有值 |
---
## 明细新增的两种方式
### 方式 A从库存选择带入
1. 查询逻辑:
- 通过 `wms_structattr.storagevehicle_code = wms_group_plate.vehicle_code` 关联仓库
- 过滤条件:`storId` = 主表所选仓库、`status` = 可用、`qty - frozenQty > 0`
- 支持物料编码、批次、载具编码等可选筛选条件
2. **一对多联动**:一个 `vehicle_code` 对应多个 `pcsn`,勾选其中一条时,同一 `vehicle_code` 的所有记录自动勾选并带入明细
3. 出库重量默认带入可用数量(`qty - frozenQty`),用户可编辑
### 方式 B手动新增汇总
1. 弹出简单表单:选择物料、输入出库重量、批次(可选)、备注(可选)
2. 单位自动带出物料的基本计量单位
3. 箱号、子卷号为空(仅页面不展示,不存 `pcsn`
---
## 后端设计
### 新增接口
**`POST /wms/iostor-inv/create-with-details`**
```java
@PostMapping("/create-with-details")
@Operation(summary = "创建出库单(含明细)")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")
public CommonResult<String> createIostorInvWithDetails(
@Valid @RequestBody IostorInvWithDetailsSaveReqVO reqVO)
```
### 请求 VO
```java
public class IostorInvWithDetailsSaveReqVO {
// 主表字段
private String billType; // 业务类型
private String storId; // 仓库标识
private String storCode; // 仓库编码
private String storName; // 仓库名称
private LocalDateTime bizDate; // 业务日期
private String remark; // 备注
// 明细列表
private List<DetailVO> details;
}
public class DetailVO {
private String materialCode;
private String materialId;
private String pcsn; // 子卷号/批次
private BigDecimal planQty; // 出库重量
private String qtyUnitId;
private String qtyUnitName;
private String remark;
private String sourceBillCode; // 来源单据号
private String sourceBillType; // 来源单据类型
private String sourceBilldtlId; // 来源单据明细标识
}
```
### Service 层逻辑
1. 调用 `CodeGenApi.generate("IO_CODE")` 生成单据号
2. 设置 `ioType = "OUT"``billStatus = "生成"`
3. 计算 `detailCount = details.size()``totalWeight = sum(details.planQty)`
4. 插入主表 `wms_iostorinv`
5. 批量插入明细 `wms_iostorinvdtl`
- `planQty` = 出库重量
- `unassignQty` = `planQty`(初始等于计划数量)
- `assignQty` = 0
- `seqNo` 自动编号
6. `@Transactional` 保证原子性
### 库存查询Mapper XML
`IostorinvDtlMapper.xml` 中新增通用查询 SQL方便后续扩展
```xml
<select id="selectAvailableInventory"
resultType="cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.AvailableInventoryVO">
SELECT
gp.vehicle_code,
gp.pcsn,
gp.material_code,
gp.material_id,
gp.qty,
gp.frozen_qty,
(gp.qty - gp.frozen_qty) AS available_qty,
gp.qty_unit_id,
gp.qty_unit_name,
gp.ext_code,
gp.ext_type,
gp.ext_dtl_code,
sa.stor_id,
sa.stor_code,
sa.stor_name
FROM wms_group_plate gp
INNER JOIN wms_structattr sa
ON gp.vehicle_code = sa.storagevehicle_code
WHERE sa.stor_id = #{storId}
AND gp.status = '可用'
AND (gp.qty - gp.frozen_qty) > 0
<if test="materialCode != null">
AND gp.material_code = #{materialCode}
</if>
<if test="pcsn != null">
AND gp.pcsn = #{pcsn}
</if>
<if test="vehicleCode != null">
AND gp.vehicle_code = #{vehicleCode}
</if>
</select>
```
Controller 通过 Service → Mapper 调用,不新增独立接口。
### 数据关联图
```
wms_bsrealstorattr (仓库)
│ storId
wms_structattr (仓位)
│ storagevehicle_code
wms_group_plate (库存/组盘)
│ 选择带入
wms_iostorinvdtl (出库明细)
│ iostorinvId
wms_iostorinv (出库单主表)
```
---
## 前端设计
### 文件结构
```
nl-ui/.../views/wms/iostorinv/
├── index.vue # 列表页 (不改)
├── data.ts # 列表 schema (不改)
├── modules/
│ ├── form.vue # [重构] 大弹窗:主表表单 + 明细 table
│ ├── inventory-select.vue # [新增] 库存选择弹窗
│ └── manual-detail.vue # [新增] 手动新增汇总弹窗
nl-ui/.../api/wms/iostorinv/
└── index.ts # [扩展] 新增 createIostorInvWithDetails
```
### form.vue 重构
- 弹窗宽度 1200px 或全屏
- 主表区域 `useVbenForm`,水平布局,字段排列参照原型图
- 明细 table 内嵌在表单下方,使用 vxe-table
- 联动:
- 仓库切换 → 清空明细
- 明细增删 → 自动更新明细数、总重量
- 单据号在弹窗打开时通过预览接口展示,提交时由后端生成
### inventory-select.vue库存选择弹窗
- 搜索区:物料编码、批次、载具编码
- 表格多选 + 同 vehicle_code 联动勾选
- 可编辑出库重量列(默认可用数量)
- 确认后返回选中数据
### manual-detail.vue手动新增弹窗
- 物料选择器(可复用 MaterialSelectModal
- 出库重量输入
- 批次输入(可选)
- 备注输入(可选)
---
## 错误处理
| 场景 | 处理方式 |
|------|---------|
| 单据号生成失败CodeGenApi 不可用) | 抛出异常,事务回滚,前端提示"单据号生成失败" |
| 未选择仓库时点"新增明细方式A" | 前端拦截,提示"请先选择仓库" |
| 提交时明细为空 | 后端校验,返回"请至少添加一条明细" |
| 库存已被其他单据消耗(并发) | 提交时校验可用数量,不足则回滚提示 |
| Feign 调用超时 | 兜底:打印日志 + 抛出业务异常 |
---
## 不考虑的内容
- 入库单场景(本次只做出库)
- 编辑/修改出库单明细(本次只做新增)
- 明细的分配/确认流程
- 回传/上传逻辑