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 @@
+
+
+
+
+
+
+
+
+
+
+