Files
huachuang/docs/superpowers/specs/2026-07-22-outbound-create-design.md
2026-07-22 09:19:46 +08:00

257 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 出库单新增页面设计
## 概述
将出入库单主表(`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 调用超时 | 兜底:打印日志 + 抛出业务异常 |
---
## 不考虑的内容
- 入库单场景(本次只做出库)
- 编辑/修改出库单明细(本次只做新增)
- 明细的分配/确认流程
- 回传/上传逻辑