8.4 KiB
8.4 KiB
出库单新增页面设计
概述
将出入库单主表(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:从库存选择带入
- 查询逻辑:
- 通过
wms_structattr.storagevehicle_code = wms_group_plate.vehicle_code关联仓库 - 过滤条件:
storId= 主表所选仓库、status= 可用、qty - frozenQty > 0 - 支持物料编码、批次、载具编码等可选筛选条件
- 通过
- 一对多联动:一个
vehicle_code对应多个pcsn,勾选其中一条时,同一vehicle_code的所有记录自动勾选并带入明细 - 出库重量默认带入可用数量(
qty - frozenQty),用户可编辑
方式 B:手动新增汇总
- 弹出简单表单:选择物料、输入出库重量、批次(可选)、备注(可选)
- 单位自动带出物料的基本计量单位
- 箱号、子卷号为空(仅页面不展示,不存
pcsn)
后端设计
新增接口
POST /wms/iostor-inv/create-with-details
@PostMapping("/create-with-details")
@Operation(summary = "创建出库单(含明细)")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")
public CommonResult<String> createIostorInvWithDetails(
@Valid @RequestBody IostorInvWithDetailsSaveReqVO reqVO)
请求 VO
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 层逻辑
- 调用
CodeGenApi.generate("IO_CODE")生成单据号 - 设置
ioType = "OUT"、billStatus = "生成" - 计算
detailCount = details.size()、totalWeight = sum(details.planQty) - 插入主表
wms_iostorinv - 批量插入明细
wms_iostorinvdtl:planQty= 出库重量unassignQty=planQty(初始等于计划数量)assignQty= 0seqNo自动编号
@Transactional保证原子性
库存查询(Mapper XML)
在 IostorinvDtlMapper.xml 中新增通用查询 SQL,方便后续扩展:
<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 调用超时 | 兜底:打印日志 + 抛出业务异常 |
不考虑的内容
- 入库单场景(本次只做出库)
- 编辑/修改出库单明细(本次只做新增)
- 明细的分配/确认流程
- 回传/上传逻辑