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

8.4 KiB
Raw Blame 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

@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 层逻辑

  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方便后续扩展

<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 调用超时 兜底:打印日志 + 抛出业务异常

不考虑的内容

  • 入库单场景(本次只做出库)
  • 编辑/修改出库单明细(本次只做新增)
  • 明细的分配/确认流程
  • 回传/上传逻辑