出入库

This commit is contained in:
朱昊天
2026-05-07 15:59:27 +08:00
parent 3d386ff650
commit 22ace156f9
23 changed files with 3522 additions and 22 deletions

View File

@@ -0,0 +1,98 @@
import request from '@/utils/request'
export function listStockIoOrder(query) {
return request({
url: '/gear/stockIoOrder/list',
method: 'get',
params: query
})
}
export function getStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/' + orderId,
method: 'get'
})
}
export function getStockIoOrderWithDetail(orderId) {
return request({
url: '/gear/stockIoOrder/withDetail/' + orderId,
method: 'get'
})
}
export function addStockIoOrderWithDetail(data) {
return request({
url: '/gear/stockIoOrder/withDetail',
method: 'post',
data: data
})
}
export function updateStockIoOrderWithDetail(data) {
return request({
url: '/gear/stockIoOrder/withDetail',
method: 'put',
data: data
})
}
export function delStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/' + orderId,
method: 'delete'
})
}
export function submitStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/submit/' + orderId,
method: 'post'
})
}
export function auditStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/audit/' + orderId,
method: 'post'
})
}
export function executeStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/execute/' + orderId,
method: 'post'
})
}
export function arrivalStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/arrival/' + orderId,
method: 'post'
})
}
export function finishStockIoOrder(orderId) {
return request({
url: '/gear/stockIoOrder/finish/' + orderId,
method: 'post'
})
}
export function cancelStockIoOrder(orderId, reason) {
return request({
url: '/gear/stockIoOrder/cancel/' + orderId,
method: 'post',
data: { reason: reason || '' }
})
}
export function reverseStockIoOrder(orderId, reason) {
return request({
url: '/gear/stockIoOrder/reverse/' + orderId,
method: 'post',
data: { reason: reason || '' }
})
}

View File

@@ -11,7 +11,7 @@
</div>
<!-- 选择弹窗表格+分页+底部按钮 -->
<el-dialog title="选择配料" v-model="open" width="900px" destroy-on-close>
<el-dialog title="选择配料" v-model="open" width="900px" destroy-on-close append-to-body>
<!-- 检索功能 -->
<el-form :model="searchForm" :inline="true" class="mb-4">
<el-form-item label="配料名称">
@@ -23,14 +23,34 @@
<el-option label="辅料" value="1" />
</el-select>
</el-form-item>
<el-form-item label="配料规格">
<el-input v-model="searchForm.spec" placeholder="请输入配料规格" clearable @keyup.enter="fetchMaterialList" />
</el-form-item>
<el-form-item label="配料型号">
<el-input v-model="searchForm.model" placeholder="请输入配料型号" clearable @keyup.enter="fetchMaterialList" />
</el-form-item>
<el-form-item label="生产厂家">
<el-input v-model="searchForm.factory" placeholder="请输入生产厂家" clearable @keyup.enter="fetchMaterialList" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchMaterialList">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button v-if="props.allowAdd" type="primary" plain @click="openAdd">新增原料</el-button>
</el-form-item>
</el-form>
<el-table :data="list" style="width: 100%" border stripe @row-click="handleRowSelect" highlight-current-row
row-key="materialId" :current-row-key="materialId">
<el-table
:data="list"
style="width: 100%"
border
stripe
@row-click="handleRowSelect"
@selection-change="handleSelectionChange"
highlight-current-row
row-key="materialId"
:current-row-key="materialId"
>
<el-table-column v-if="props.multiple" type="selection" width="55" align="center" />
<el-table-column prop="materialName" label="配料名称" min-width="120" align="center" />
<el-table-column label="物料类型" width="100" align="center">
<template #default="scope">
@@ -56,40 +76,111 @@
</div>
<template #footer>
<el-button v-if="props.multiple" type="primary" @click="confirmMultiple">确定</el-button>
<el-button @click="open = false">取消</el-button>
</template>
</el-dialog>
<el-dialog title="新增原料" v-model="addOpen" width="600px" destroy-on-close append-to-body>
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="90px">
<el-form-item label="原料名称" prop="materialName">
<el-input v-model="addForm.materialName" placeholder="请输入原料名称" />
</el-form-item>
<el-form-item label="归属类型" prop="materialType">
<el-select v-model="addForm.materialType" placeholder="请选择主材/辅材">
<el-option label="主材" :value="2" />
<el-option label="辅材" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="规格" prop="spec">
<el-input v-model="addForm.spec" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="型号" prop="model">
<el-input v-model="addForm.model" placeholder="请输入型号" />
</el-form-item>
<el-form-item label="厂家" prop="factory">
<el-input v-model="addForm.factory" placeholder="请输入厂家" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="addForm.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="addForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button :loading="addLoading" type="primary" @click="submitAdd">保存</el-button>
<el-button @click="addOpen = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup name="RawSelector">
import { ref, computed, watch, onMounted } from 'vue';
import { listMaterial, getMaterial } from '@/api/mat/material';
import { ref, computed, onMounted } from 'vue';
import { listMaterial, getMaterial, addMaterial } from '@/api/mat/material';
import { ElMessage } from 'element-plus';
import { formatDecimal } from '@/utils/gear'
const props = defineProps({
allowAdd: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
}
})
// 双向绑定物料ID
const materialId = defineModel({
type: String,
type: [String, Number],
default: ""
})
const materialIds = defineModel('materialIds', {
type: Array,
default: () => []
})
// 计算当前选中的物料信息
const currentMaterial = computed(() => {
if (!materialId.value) return {};
return list.value.find(item => item.materialId === materialId.value) || {};
const mid = String(materialId.value)
return list.value.find(item => String(item.materialId) === mid) || {};
});
// 定义事件
const emit = defineEmits(['change']);
const emit = defineEmits(['change', 'multiChange']);
// 基础响应式数据
const list = ref([]); // 物料列表
const open = ref(false); // 弹窗显隐
const selectedRows = ref([])
const addOpen = ref(false)
const addLoading = ref(false)
const addFormRef = ref()
const addForm = ref({
materialName: '',
materialType: undefined,
spec: '',
model: '',
factory: '',
unit: '',
remark: ''
})
const addRules = {
materialName: [{ required: true, message: '原料名称不能为空', trigger: 'blur' }],
materialType: [{ required: true, message: '请选择归属类型', trigger: 'change' }]
}
// 搜索表单数据
const searchForm = ref({
materialName: '',
materialType: ''
materialType: '',
spec: '',
model: '',
factory: ''
});
// 分页响应式数据(核心新增)
@@ -97,6 +188,10 @@ const pageNum = ref(1); // 当前页码
const pageSize = ref(10); // 每页条数
const total = ref(0); // 总数据条数
function formatUnit(row) {
return row && row.unit ? row.unit : ''
}
// 组件挂载加载列表
onMounted(async () => {
await fetchMaterialList();
@@ -110,7 +205,10 @@ async function fetchMaterialList() {
pageNum: pageNum.value,
pageSize: pageSize.value,
materialName: searchForm.value.materialName,
materialType: searchForm.value.materialType
materialType: searchForm.value.materialType,
spec: searchForm.value.spec,
model: searchForm.value.model,
factory: searchForm.value.factory
};
const res = await listMaterial(params);
@@ -118,9 +216,9 @@ async function fetchMaterialList() {
list.value = res.rows;
total.value = res.total; // 赋值总条数供分页使用
// 分页查询后检查是否存在当前选中的物料ID
const isExist = res.rows.some(item => item.materialId === materialId.value);
const mid = String(materialId.value || '')
const isExist = res.rows.some(item => String(item.materialId) === mid);
if (!isExist) {
console.log('获取物料详情:', materialId.value);
if (!materialId.value) return;
const res = await getMaterial(materialId.value);
list.value.push(res.data);
@@ -135,7 +233,10 @@ async function fetchMaterialList() {
function resetSearch() {
searchForm.value = {
materialName: '',
materialType: ''
materialType: '',
spec: '',
model: '',
factory: ''
};
pageNum.value = 1;
fetchMaterialList();
@@ -143,16 +244,84 @@ function resetSearch() {
// 表格行选择物料
function handleRowSelect(row) {
if (row.materialId === materialId.value) return;
if (props.multiple) {
return
}
if (String(row.materialId) === String(materialId.value)) return;
materialId.value = row.materialId;
emit('change', row);
open.value = false;
}
function handleSelectionChange(rows) {
selectedRows.value = rows || []
}
function confirmMultiple() {
const rows = selectedRows.value || []
materialIds.value = rows.map(r => r.materialId)
emit('multiChange', rows)
open.value = false
}
// 清空已选物料
function handleClear() {
materialId.value = '';
emit('change', {});
ElMessage.info('已清空配料选择');
}
</script>
function openAdd() {
addForm.value = {
materialName: searchForm.value.materialName || '',
materialType: searchForm.value.materialType ? Number(searchForm.value.materialType) : undefined,
spec: '',
model: '',
factory: '',
unit: '',
remark: ''
}
addOpen.value = true
}
function submitAdd() {
if (!addFormRef.value) {
return
}
addFormRef.value.validate(async (valid) => {
if (!valid) return
addLoading.value = true
try {
const payload = Object.assign({}, addForm.value)
const res = await addMaterial(payload)
const newId = res && res.data
if (!newId) {
ElMessage.success('新增成功')
addOpen.value = false
await fetchMaterialList()
return
}
materialId.value = newId
const detailRes = await getMaterial(newId)
const row = detailRes && detailRes.data ? detailRes.data : null
if (row) {
list.value = [row].concat(list.value.filter(i => String(i.materialId) !== String(newId)))
emit('change', row)
}
ElMessage.success('新增成功')
addOpen.value = false
open.value = false
} catch (e) {
ElMessage.error('新增失败,请重试')
} finally {
addLoading.value = false
}
})
}
function openDialog() {
open.value = true
}
defineExpose({ openDialog })
</script>

View File

@@ -68,8 +68,6 @@
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleIn(scope.row)">入库</el-button>
<el-button link type="primary" icon="Minus" @click="handleOut(scope.row)">出库</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
@@ -461,4 +459,4 @@ function submitFormOut() {
}
getList();
</script>
</script>

View File

@@ -72,8 +72,6 @@
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleIn(scope.row)">入库</el-button>
<el-button link type="primary" icon="Minus" @click="handleOut(scope.row)">出库</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>

View File

@@ -0,0 +1,514 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="单号" prop="orderCode">
<el-input v-model="queryParams.orderCode" placeholder="请输入单据编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-input v-model="queryParams.bizType" placeholder="请输入业务类型" clearable />
</el-form-item>
<el-form-item label="责任人" prop="responsibleName">
<el-input v-model="queryParams.responsibleName" placeholder="请输入责任人" clearable />
</el-form-item>
<el-form-item label="来源单号" prop="sourceNo">
<el-input v-model="queryParams.sourceNo" placeholder="请输入来源单号" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Clock" size="mini" :disabled="single" @click="handleArrival()">到货确认</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="RefreshLeft" size="mini" :disabled="single" @click="handleReverse()">撤回</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="单据编号" align="center" prop="orderCode" min-width="160" />
<el-table-column label="物料" align="center" min-width="220">
<template #default="scope">
<el-tooltip v-if="scope.row.materialNames && String(scope.row.materialNames).length > 16" effect="dark" placement="top">
<template #content>
<div style="max-width: 420px; white-space: normal; word-break: break-all;">
{{ scope.row.materialNames }}
</div>
</template>
<span>{{ String(scope.row.materialNames).slice(0, 16) + '…' }}</span>
</el-tooltip>
<span v-else>{{ scope.row.materialNames || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="业务类型" align="center" prop="bizType" min-width="120" />
<el-table-column label="责任人" align="center" prop="responsibleName" min-width="120" />
<el-table-column label="实际到货" align="center" prop="actualArrivalTime" min-width="160" />
<el-table-column label="来源单号" align="center" prop="sourceNo" min-width="140" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-button size="mini" type="text" icon="Document" @click="showDetail(scope.row)">明细</el-button>
<el-button size="mini" type="text" icon="Clock" @click="handleArrival(scope.row)">到货确认</el-button>
<el-button size="mini" type="text" icon="RefreshLeft" @click="handleReverse(scope.row)">撤回</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="editTitle" v-model="editOpen" width="1100px" top="5vh" append-to-body>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="90px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="类型">
<el-tag type="success">入库</el-tag>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="业务类型" prop="bizType">
<el-input v-model="editForm.bizType" placeholder="请输入业务类型" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="责任人" prop="responsibleName">
<el-input v-model="editForm.responsibleName" placeholder="请输入责任人" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="来源单号" prop="sourceNo">
<el-input v-model="editForm.sourceNo" placeholder="请输入来源单号" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px;">
<el-button type="primary" plain icon="Plus" size="mini" @click="addDetailRow">新增明细行</el-button>
<RawSelector v-model:materialIds="batchMaterialIds" :multiple="true" :allowAdd="true" @multi-change="handleBatchPicked">
<template #trigger>
<el-button type="primary" plain size="mini">批量选择原料</el-button>
</template>
</RawSelector>
<el-button type="danger" plain icon="Delete" size="mini" :disabled="detailSelection.length === 0" @click="removeDetailRows">删除选中</el-button>
<div style="margin-left:auto;">
<span>合计数量{{ totalQtyText }}</span>
</div>
</div>
<el-table :data="editDetails" border style="width: 100%" @selection-change="onDetailSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="行号" width="70" align="center">
<template #default="scope">
{{ scope.row.lineNo }}
</template>
</el-table-column>
<el-table-column label="物料类型" width="100" align="center">
<template #default>
原料
</template>
</el-table-column>
<el-table-column label="原料" min-width="260" align="center">
<template #default="scope">
<RawSelector v-model="scope.row.itemId" :allowAdd="true" @change="onMaterialPicked(scope.row, $event)" />
</template>
</el-table-column>
<el-table-column label="名称快照" min-width="180" align="center">
<template #default="scope">
<el-input v-model="scope.row.itemName" placeholder="可选" />
</template>
</el-table-column>
<el-table-column label="数量" min-width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.quantity" placeholder="请输入数量" @input="recalcAmount(scope.row)" />
</template>
</el-table-column>
<el-table-column label="单位" width="90" align="center">
<template #default="scope">
<el-input v-model="scope.row.unit" placeholder="单位" />
</template>
</el-table-column>
<el-table-column label="批次号" min-width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.batchNo" placeholder="批次号" />
</template>
</el-table-column>
<el-table-column label="单价" min-width="110" align="center">
<template #default="scope">
<el-input v-model="scope.row.unitPrice" placeholder="单价" @input="recalcAmount(scope.row)" />
</template>
</el-table-column>
<el-table-column label="金额" min-width="110" align="center">
<template #default="scope">
<el-input v-model="scope.row.amount" placeholder="金额" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="备注" />
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitEdit">保存</el-button>
<el-button @click="editOpen = false">取消</el-button>
</template>
</el-dialog>
<el-dialog title="单据明细" v-model="detailOpen" width="1100px" append-to-body>
<template v-if="detailData && detailData.order">
<el-descriptions :title="'单号:' + (detailData.order.orderCode || '-')" :column="2" border>
<el-descriptions-item label="类型">入库</el-descriptions-item>
<el-descriptions-item label="业务类型">{{ detailData.order.bizType }}</el-descriptions-item>
<el-descriptions-item label="责任人">{{ detailData.order.responsibleName }}</el-descriptions-item>
<el-descriptions-item label="实际到货">{{ detailData.order.actualArrivalTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div style="margin: 12px 0; text-align: right;">
<el-button type="warning" size="small" @click="handleArrival(detailData.order)">到货确认</el-button>
<el-button type="danger" size="small" @click="handleReverse(detailData.order)">撤回</el-button>
</div>
<el-table :data="detailData.details || []" border style="width: 100%">
<el-table-column label="行号" prop="lineNo" width="70" align="center" />
<el-table-column label="原料ID" prop="itemId" width="120" align="center" />
<el-table-column label="名称快照" prop="itemName" min-width="160" align="center" />
<el-table-column label="数量" prop="quantity" width="120" align="center" />
<el-table-column label="单位" prop="unit" width="90" align="center" />
<el-table-column label="批次号" prop="batchNo" min-width="120" align="center" />
<el-table-column label="备注" prop="remark" min-width="140" align="center" />
</el-table>
</template>
<template v-else>
<div style="height:200px;line-height:200px;text-align:center;">未获取到单据数据</div>
</template>
<template #footer>
<el-button @click="detailOpen = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import RawSelector from '@/components/RawSelector/index.vue'
import {
listStockIoOrder,
getStockIoOrderWithDetail,
addStockIoOrderWithDetail,
arrivalStockIoOrder,
reverseStockIoOrder
} from '@/api/wms/stockIoOrder'
export default {
name: 'StockIoOrderIn',
components: { RawSelector },
data() {
return {
loading: true,
buttonLoading: false,
ids: [],
rows: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
list: [],
queryParams: {
pageNum: 1,
pageSize: 20,
orderCode: undefined,
ioType: 'I',
bizType: undefined,
responsibleName: undefined,
sourceNo: undefined
},
editOpen: false,
editTitle: '',
editForm: {},
editDetails: [],
batchMaterialIds: [],
detailSelection: [],
editRules: {
bizType: [{ required: true, message: '业务类型不能为空', trigger: 'blur' }]
},
detailOpen: false,
detailData: null
}
},
computed: {
totalQtyText() {
const total = (this.editDetails || []).reduce((sum, r) => sum + this.toNumber(r.quantity), 0)
return String(total)
}
},
created() {
this.getList()
},
methods: {
toNumber(val) {
const n = Number(val)
return Number.isFinite(n) ? n : 0
},
getList() {
this.loading = true
listStockIoOrder(this.queryParams)
.then((res) => {
this.list = res.rows || []
this.total = res.total || 0
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.queryParams.ioType = 'I'
this.handleQuery()
},
handleSelectionChange(selection) {
this.rows = selection
this.ids = selection.map((r) => r.orderId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
resetEdit() {
this.editForm = {
ioType: 'I',
bizType: undefined,
sourceNo: undefined,
responsibleName: undefined,
remark: undefined
}
this.editDetails = []
this.detailSelection = []
this.resetForm('editFormRef')
},
handleAdd() {
this.resetEdit()
this.editTitle = '新增入库单据'
this.editOpen = true
this.addDetailRow()
},
showDetail(row) {
this.detailOpen = true
this.detailData = null
getStockIoOrderWithDetail(row.orderId).then((res) => {
this.detailData = res.data
})
},
handleArrival(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认到货?')
.then(() => {
this.buttonLoading = true
return arrivalStockIoOrder(target.orderId)
})
.then(() => {
this.$modal.msgSuccess('已确认到货')
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
},
handleReverse(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认冲销该单据?系统将创建一张反向单据并自动执行。')
.then(() => {
this.buttonLoading = true
return reverseStockIoOrder(target.orderId, '')
})
.then((res) => {
const newId = res.data
this.$modal.msgSuccess('冲销成功冲销单ID' + newId)
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
},
submitEdit() {
this.$refs.editFormRef.validate((valid) => {
if (!valid) return
if (!this.editDetails || this.editDetails.length === 0) {
this.$modal.msgError('请先添加明细')
return
}
const check = this.validateDetails()
if (!check.ok) {
this.$modal.msgError(check.message)
return
}
const payload = this.buildPayload()
if (!payload.details || payload.details.length === 0) {
this.$modal.msgError('请至少填写一条明细:选择原料并输入数量')
return
}
this.buttonLoading = true
addStockIoOrderWithDetail(payload)
.then(() => {
this.$modal.msgSuccess('保存成功')
this.editOpen = false
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
})
},
validateDetails() {
for (const d of this.editDetails || []) {
if (!d) continue
const hasAny =
(d.itemId != null && String(d.itemId) !== '') ||
(d.quantity != null && String(d.quantity) !== '') ||
(d.unitPrice != null && String(d.unitPrice) !== '') ||
(d.batchNo != null && String(d.batchNo) !== '') ||
(d.remark != null && String(d.remark) !== '') ||
(d.itemName != null && String(d.itemName) !== '')
if (!hasAny) {
continue
}
const lineNo = d.lineNo != null ? d.lineNo : '-'
if (d.itemId == null || String(d.itemId) === '') {
return { ok: false, message: `${lineNo}行请选择原料` }
}
const qty = this.toNumber(d.quantity)
if (qty <= 0) {
return { ok: false, message: `${lineNo}行请输入数量` }
}
}
return { ok: true, message: '' }
},
buildPayload() {
const details = (this.editDetails || [])
.filter((d) => d && d.itemId != null && String(d.itemId) !== '' && this.toNumber(d.quantity) > 0)
.map((d, idx) => {
const lineNo = d.lineNo != null ? d.lineNo : idx + 1
const unitPrice = this.toNumber(d.unitPrice)
const quantity = this.toNumber(d.quantity)
const amount = d.amount != null && String(d.amount) !== '' ? this.toNumber(d.amount) : unitPrice * quantity
return {
detailId: d.detailId,
lineNo,
itemType: 'material',
itemId: d.itemId,
itemName: d.itemName,
quantity: quantity,
unit: d.unit,
batchNo: d.batchNo,
unitPrice: unitPrice,
amount: amount,
remark: d.remark
}
})
const totalQty = details.reduce((sum, d) => sum + this.toNumber(d.quantity), 0)
return Object.assign({}, this.editForm, { ioType: 'I', totalQty, details })
},
addDetailRow() {
const nextNo = (this.editDetails || []).length + 1
this.editDetails.push({
detailId: undefined,
lineNo: nextNo,
itemType: 'material',
itemId: undefined,
itemName: undefined,
quantity: undefined,
unit: undefined,
batchNo: undefined,
unitPrice: undefined,
amount: undefined,
remark: undefined
})
this.rebuildLineNo()
},
onDetailSelectionChange(rows) {
this.detailSelection = rows || []
},
removeDetailRows() {
const ids = new Set((this.detailSelection || []).map((r) => r.lineNo))
this.editDetails = (this.editDetails || []).filter((r) => !ids.has(r.lineNo))
this.detailSelection = []
if (this.editDetails.length === 0) {
this.addDetailRow()
} else {
this.rebuildLineNo()
}
},
rebuildLineNo() {
let i = 1
this.editDetails = (this.editDetails || []).map((r) => Object.assign({}, r, { lineNo: i++ }))
},
handleBatchPicked(materials) {
const rows = materials || []
if (rows.length === 0) {
return
}
let startIndex = 0
if (this.editDetails.length === 1) {
const r = this.editDetails[0]
const empty =
(r.itemId == null || String(r.itemId) === '') &&
(r.quantity == null || String(r.quantity) === '') &&
(r.unitPrice == null || String(r.unitPrice) === '')
if (empty) {
r.itemId = rows[0].materialId
this.onMaterialPicked(r, rows[0])
startIndex = 1
}
}
for (let i = startIndex; i < rows.length; i++) {
this.addDetailRow()
const r = this.editDetails[this.editDetails.length - 1]
r.itemId = rows[i].materialId
this.onMaterialPicked(r, rows[i])
}
this.batchMaterialIds = []
},
onMaterialPicked(row, material) {
if (material && material.materialName) {
row.itemName = material.materialName
if (!row.unit && material.unit) {
row.unit = material.unit
}
if (material.unitPrice != null && row.unitPrice == null) {
row.unitPrice = material.unitPrice
}
this.recalcAmount(row)
}
},
recalcAmount(row) {
const qty = this.toNumber(row.quantity)
const unitPrice = this.toNumber(row.unitPrice)
if (qty > 0 || unitPrice > 0) {
row.amount = String(unitPrice * qty)
}
}
}
}
</script>

View File

@@ -0,0 +1,12 @@
<template>
<StockIoOrderPage />
</template>
<script>
import StockIoOrderPage from './panels/stockIoOrderPage.vue'
export default {
name: 'StockIoOrder',
components: { StockIoOrderPage }
}
</script>

View File

@@ -0,0 +1,486 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="单号" prop="orderCode">
<el-input v-model="queryParams.orderCode" placeholder="请输入单据编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-input v-model="queryParams.bizType" placeholder="请输入业务类型" clearable />
</el-form-item>
<el-form-item label="责任人" prop="responsibleName">
<el-input v-model="queryParams.responsibleName" placeholder="请输入责任人" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="RefreshLeft" size="mini" :disabled="single" @click="handleRevoke()">撤回</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="单据编号" align="center" prop="orderCode" min-width="160" />
<el-table-column label="物料" align="center" min-width="220">
<template #default="scope">
<el-tooltip v-if="scope.row.materialNames && String(scope.row.materialNames).length > 16" effect="dark" placement="top">
<template #content>
<div style="max-width: 420px; white-space: normal; word-break: break-all;">
{{ scope.row.materialNames }}
</div>
</template>
<span>{{ String(scope.row.materialNames).slice(0, 16) + '…' }}</span>
</el-tooltip>
<span v-else>{{ scope.row.materialNames || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="业务类型" align="center" prop="bizType" min-width="120" />
<el-table-column label="责任人" align="center" prop="responsibleName" min-width="120" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-button size="mini" type="text" icon="Document" @click="showDetail(scope.row)">明细</el-button>
<el-button size="mini" type="text" icon="RefreshLeft" @click="handleRevoke(scope.row)">撤回</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="editTitle" v-model="editOpen" width="1100px" top="5vh" append-to-body>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="90px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="类型">
<el-tag type="warning">出库</el-tag>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="业务类型" prop="bizType">
<el-input v-model="editForm.bizType" placeholder="请输入业务类型" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="责任人" prop="responsibleName">
<el-input v-model="editForm.responsibleName" placeholder="请输入责任人" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="来源单号" prop="sourceNo">
<el-input v-model="editForm.sourceNo" placeholder="请输入来源单号" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px;">
<el-button type="primary" plain icon="Plus" size="mini" @click="addDetailRow">新增明细行</el-button>
<RawSelector v-model:materialIds="batchMaterialIds" :multiple="true" :allowAdd="false" @multi-change="handleBatchPicked">
<template #trigger>
<el-button type="primary" plain size="mini">批量选择原料</el-button>
</template>
</RawSelector>
<el-button type="danger" plain icon="Delete" size="mini" :disabled="detailSelection.length === 0" @click="removeDetailRows">删除选中</el-button>
<div style="margin-left:auto;">
<span>合计数量{{ totalQtyText }}</span>
</div>
</div>
<el-table :data="editDetails" border style="width: 100%" @selection-change="onDetailSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="行号" width="70" align="center">
<template #default="scope">
{{ scope.row.lineNo }}
</template>
</el-table-column>
<el-table-column label="物料类型" width="100" align="center">
<template #default>
原料
</template>
</el-table-column>
<el-table-column label="原料" min-width="260" align="center">
<template #default="scope">
<RawSelector v-model="scope.row.itemId" :allowAdd="false" @change="onMaterialPicked(scope.row, $event)" />
</template>
</el-table-column>
<el-table-column label="名称快照" min-width="180" align="center">
<template #default="scope">
<el-input v-model="scope.row.itemName" placeholder="可选" />
</template>
</el-table-column>
<el-table-column label="数量" min-width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.quantity" placeholder="请输入数量" @input="recalcAmount(scope.row)" />
</template>
</el-table-column>
<el-table-column label="单位" width="90" align="center">
<template #default="scope">
<el-input v-model="scope.row.unit" placeholder="单位" />
</template>
</el-table-column>
<el-table-column label="批次号" min-width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.batchNo" placeholder="批次号" />
</template>
</el-table-column>
<el-table-column label="单价" min-width="110" align="center">
<template #default="scope">
<el-input v-model="scope.row.unitPrice" placeholder="单价" @input="recalcAmount(scope.row)" />
</template>
</el-table-column>
<el-table-column label="金额" min-width="110" align="center">
<template #default="scope">
<el-input v-model="scope.row.amount" placeholder="金额" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="备注" />
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitEdit">出库</el-button>
<el-button @click="editOpen = false">取消</el-button>
</template>
</el-dialog>
<el-dialog title="单据明细" v-model="detailOpen" width="1100px" append-to-body>
<template v-if="detailData && detailData.order">
<el-descriptions :title="'单号:' + (detailData.order.orderCode || '-')" :column="2" border>
<el-descriptions-item label="类型">出库</el-descriptions-item>
<el-descriptions-item label="业务类型">{{ detailData.order.bizType }}</el-descriptions-item>
<el-descriptions-item label="责任人">{{ detailData.order.responsibleName }}</el-descriptions-item>
</el-descriptions>
<div style="margin: 12px 0; text-align: right;">
<el-button type="danger" size="small" @click="handleRevoke(detailData.order)">撤回</el-button>
</div>
<el-table :data="detailData.details || []" border style="width: 100%">
<el-table-column label="行号" prop="lineNo" width="70" align="center" />
<el-table-column label="原料ID" prop="itemId" width="120" align="center" />
<el-table-column label="名称快照" prop="itemName" min-width="160" align="center" />
<el-table-column label="数量" prop="quantity" width="120" align="center" />
<el-table-column label="单位" prop="unit" width="90" align="center" />
<el-table-column label="批次号" prop="batchNo" min-width="120" align="center" />
<el-table-column label="备注" prop="remark" min-width="140" align="center" />
</el-table>
</template>
<template v-else>
<div style="height:200px;line-height:200px;text-align:center;">未获取到单据数据</div>
</template>
<template #footer>
<el-button @click="detailOpen = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import RawSelector from '@/components/RawSelector/index.vue'
import {
listStockIoOrder,
getStockIoOrderWithDetail,
addStockIoOrderWithDetail,
reverseStockIoOrder
} from '@/api/wms/stockIoOrder'
export default {
name: 'StockIoOrderOut',
components: { RawSelector },
data() {
return {
loading: true,
buttonLoading: false,
ids: [],
rows: [],
single: true,
showSearch: true,
total: 0,
list: [],
queryParams: {
pageNum: 1,
pageSize: 20,
orderCode: undefined,
ioType: 'O',
bizType: undefined,
responsibleName: undefined
},
editOpen: false,
editTitle: '',
editForm: {},
editDetails: [],
batchMaterialIds: [],
detailSelection: [],
editRules: {
bizType: [{ required: true, message: '业务类型不能为空', trigger: 'blur' }]
},
detailOpen: false,
detailData: null
}
},
computed: {
totalQtyText() {
const total = (this.editDetails || []).reduce((sum, r) => sum + this.toNumber(r.quantity), 0)
return String(total)
}
},
created() {
this.getList()
},
methods: {
toNumber(val) {
const n = Number(val)
return Number.isFinite(n) ? n : 0
},
getList() {
this.loading = true
listStockIoOrder(this.queryParams)
.then((res) => {
this.list = res.rows || []
this.total = res.total || 0
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.queryParams.ioType = 'O'
this.handleQuery()
},
handleSelectionChange(selection) {
this.rows = selection
this.ids = selection.map((r) => r.orderId)
this.single = selection.length !== 1
},
resetEdit() {
this.editForm = {
ioType: 'O',
bizType: undefined,
sourceNo: undefined,
responsibleName: undefined,
remark: undefined
}
this.editDetails = []
this.detailSelection = []
this.resetForm('editFormRef')
},
handleAdd() {
this.resetEdit()
this.editTitle = '新增出库单据'
this.editOpen = true
this.addDetailRow()
},
showDetail(row) {
this.detailOpen = true
this.detailData = null
getStockIoOrderWithDetail(row.orderId).then((res) => {
this.detailData = res.data
})
},
handleRevoke(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认撤回该出库?系统将创建一张反向单据并自动执行。')
.then(() => {
this.buttonLoading = true
return reverseStockIoOrder(target.orderId, '')
})
.then((res) => {
const newId = res.data
this.$modal.msgSuccess('撤回成功撤回单ID' + newId)
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
},
submitEdit() {
this.$refs.editFormRef.validate((valid) => {
if (!valid) return
if (!this.editDetails || this.editDetails.length === 0) {
this.$modal.msgError('请先添加明细')
return
}
const check = this.validateDetails()
if (!check.ok) {
this.$modal.msgError(check.message)
return
}
const payload = this.buildPayload()
if (payload.orderId) {
this.$modal.msgError('已出库单据不支持修改,请撤回后重新出库')
return
}
if (!payload.details || payload.details.length === 0) {
this.$modal.msgError('请至少填写一条明细:选择原料并输入数量')
return
}
this.buttonLoading = true
addStockIoOrderWithDetail(payload)
.then(() => {
this.$modal.msgSuccess('出库成功')
this.editOpen = false
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
})
},
validateDetails() {
for (const d of this.editDetails || []) {
if (!d) continue
const hasAny =
(d.itemId != null && String(d.itemId) !== '') ||
(d.quantity != null && String(d.quantity) !== '') ||
(d.unitPrice != null && String(d.unitPrice) !== '') ||
(d.batchNo != null && String(d.batchNo) !== '') ||
(d.remark != null && String(d.remark) !== '') ||
(d.itemName != null && String(d.itemName) !== '')
if (!hasAny) {
continue
}
const lineNo = d.lineNo != null ? d.lineNo : '-'
if (d.itemId == null || String(d.itemId) === '') {
return { ok: false, message: `${lineNo}行请选择原料` }
}
const qty = this.toNumber(d.quantity)
if (qty <= 0) {
return { ok: false, message: `${lineNo}行请输入数量` }
}
}
return { ok: true, message: '' }
},
buildPayload() {
const details = (this.editDetails || [])
.filter((d) => d && d.itemId != null && String(d.itemId) !== '' && this.toNumber(d.quantity) > 0)
.map((d, idx) => {
const lineNo = d.lineNo != null ? d.lineNo : idx + 1
const unitPrice = this.toNumber(d.unitPrice)
const quantity = this.toNumber(d.quantity)
const amount = d.amount != null && String(d.amount) !== '' ? this.toNumber(d.amount) : unitPrice * quantity
return {
detailId: d.detailId,
lineNo,
itemType: 'material',
itemId: d.itemId,
itemName: d.itemName,
quantity: quantity,
unit: d.unit,
batchNo: d.batchNo,
unitPrice: unitPrice,
amount: amount,
remark: d.remark
}
})
const totalQty = details.reduce((sum, d) => sum + this.toNumber(d.quantity), 0)
return Object.assign({}, this.editForm, { ioType: 'O', totalQty, details })
},
addDetailRow() {
const nextNo = (this.editDetails || []).length + 1
this.editDetails.push({
detailId: undefined,
lineNo: nextNo,
itemType: 'material',
itemId: undefined,
itemName: undefined,
quantity: undefined,
unit: undefined,
batchNo: undefined,
unitPrice: undefined,
amount: undefined,
remark: undefined
})
this.rebuildLineNo()
},
onDetailSelectionChange(rows) {
this.detailSelection = rows || []
},
removeDetailRows() {
const ids = new Set((this.detailSelection || []).map((r) => r.lineNo))
this.editDetails = (this.editDetails || []).filter((r) => !ids.has(r.lineNo))
this.detailSelection = []
if (this.editDetails.length === 0) {
this.addDetailRow()
} else {
this.rebuildLineNo()
}
},
rebuildLineNo() {
let i = 1
this.editDetails = (this.editDetails || []).map((r) => Object.assign({}, r, { lineNo: i++ }))
},
handleBatchPicked(materials) {
const rows = materials || []
if (rows.length === 0) {
return
}
let startIndex = 0
if (this.editDetails.length === 1) {
const r = this.editDetails[0]
const empty =
(r.itemId == null || String(r.itemId) === '') &&
(r.quantity == null || String(r.quantity) === '') &&
(r.unitPrice == null || String(r.unitPrice) === '')
if (empty) {
r.itemId = rows[0].materialId
this.onMaterialPicked(r, rows[0])
startIndex = 1
}
}
for (let i = startIndex; i < rows.length; i++) {
this.addDetailRow()
const r = this.editDetails[this.editDetails.length - 1]
r.itemId = rows[i].materialId
this.onMaterialPicked(r, rows[i])
}
this.batchMaterialIds = []
},
onMaterialPicked(row, material) {
if (material && material.materialName) {
row.itemName = material.materialName
if (!row.unit && material.unit) {
row.unit = material.unit
}
if (material.unitPrice != null && row.unitPrice == null) {
row.unitPrice = material.unitPrice
}
this.recalcAmount(row)
}
},
recalcAmount(row) {
const qty = this.toNumber(row.quantity)
const unitPrice = this.toNumber(row.unitPrice)
if (qty > 0 || unitPrice > 0) {
row.amount = String(unitPrice * qty)
}
}
}
}
</script>

View File

@@ -0,0 +1,808 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="单号" prop="orderCode">
<el-input v-model="queryParams.orderCode" placeholder="请输入单据编号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item v-if="showIoTypeSelector" label="类型" prop="ioType">
<el-select v-model="queryParams.ioType" placeholder="请选择类型" clearable style="width: 120px">
<el-option v-for="it in ioTypeOptions" :key="it.value" :label="it.label" :value="it.value" />
</el-select>
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-input v-model="queryParams.bizType" placeholder="请输入业务类型" clearable />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 120px">
<el-option v-for="it in statusOptions" :key="it.value" :label="it.label" :value="it.value" />
</el-select>
</el-form-item>
<el-form-item label="已执行" prop="execFlag">
<el-select v-model="queryParams.execFlag" placeholder="请选择" clearable style="width: 120px">
<el-option v-for="it in yesNoOptions" :key="it.value" :label="it.label" :value="it.value" />
</el-select>
</el-form-item>
<el-form-item label="延迟" prop="delayStatus">
<el-select v-model="queryParams.delayStatus" placeholder="请选择" clearable style="width: 120px">
<el-option v-for="it in delayStatusOptions" :key="it.value" :label="it.label" :value="it.value" />
</el-select>
</el-form-item>
<el-form-item label="责任人" prop="responsibleName">
<el-input v-model="queryParams.responsibleName" placeholder="请输入责任人" clearable />
</el-form-item>
<el-form-item label="来源单号" prop="sourceNo">
<el-input v-model="queryParams.sourceNo" placeholder="请输入来源单号" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="CircleCheck" size="mini" :disabled="single" @click="handleAudit()">审核</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="Check" size="mini" :disabled="single" @click="handleExecute()">执行</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Clock" size="mini" :disabled="single" @click="handleArrival()">到货确认</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Finished" size="mini" :disabled="single" @click="handleFinish()">完成</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="RefreshLeft" size="mini" :disabled="single" @click="handleReverse()">冲销</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Close" size="mini" :disabled="single" @click="handleCancel()">作废</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="单据ID" align="center" prop="orderId" v-if="false" />
<el-table-column label="单据编号" align="center" prop="orderCode" min-width="160" />
<el-table-column label="类型" align="center" prop="ioType" width="90">
<template #default="scope">
<el-tag :type="ioTypeTag(scope.row.ioType)">{{ ioTypeLabel(scope.row.ioType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="业务类型" align="center" prop="bizType" min-width="120" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'info'">{{ scope.row.status === '1' ? '已完成' : '进行中' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="已执行" align="center" prop="execFlag" width="90">
<template #default="scope">
<el-tag :type="scope.row.execFlag === '1' ? 'success' : 'warning'">{{ scope.row.execFlag === '1' ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="责任人" align="center" prop="responsibleName" min-width="120" />
<el-table-column label="预计到货" align="center" prop="planArrivalTime" min-width="160" />
<el-table-column label="实际到货" align="center" prop="actualArrivalTime" min-width="160" />
<el-table-column label="预计完成" align="center" prop="planFinishTime" min-width="160" />
<el-table-column label="实际完成" align="center" prop="actualFinishTime" min-width="160" />
<el-table-column label="延迟(分钟)" align="center" prop="delayMinutes" width="110" />
<el-table-column label="延迟状态" align="center" prop="delayStatus" width="100">
<template #default="scope">
<el-tag :type="delayStatusTag(scope.row.delayStatus)">{{ delayStatusLabel(scope.row.delayStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="来源单号" align="center" prop="sourceNo" min-width="140" />
<el-table-column label="执行流水ID" align="center" prop="sourceIoId" min-width="120" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-button size="mini" type="text" icon="Document" @click="showDetail(scope.row)">明细</el-button>
<el-button size="mini" type="text" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="editTitle" v-model="editOpen" width="1200px" append-to-body>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="出入库类型" prop="ioType">
<el-select v-model="editForm.ioType" placeholder="请选择类型" style="width: 100%" :disabled="!showIoTypeSelector" @change="onIoTypeChanged">
<el-option v-for="it in ioTypeOptions" :key="it.value" :label="it.label" :value="it.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="业务类型" prop="bizType">
<el-input v-model="editForm.bizType" placeholder="请输入业务类型" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="责任人" prop="responsibleName">
<el-input v-model="editForm.responsibleName" placeholder="请输入责任人" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="来源单号" prop="sourceNo">
<el-input v-model="editForm.sourceNo" placeholder="请输入来源单号" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="主仓库" prop="warehouseId">
<WarehouseSelect v-model="editForm.warehouseId" placeholder="请选择仓库" />
</el-form-item>
</el-col>
<el-col :span="6" v-if="editForm.ioType === 'T'">
<el-form-item label="调出仓库" prop="fromWarehouseId">
<WarehouseSelect v-model="editForm.fromWarehouseId" placeholder="请选择调出仓库" />
</el-form-item>
</el-col>
<el-col :span="6" v-if="editForm.ioType === 'T'">
<el-form-item label="调入仓库" prop="toWarehouseId">
<WarehouseSelect v-model="editForm.toWarehouseId" placeholder="请选择调入仓库" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预计到货" prop="planArrivalTime">
<el-date-picker v-model="editForm.planArrivalTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预计完成" prop="planFinishTime">
<el-date-picker v-model="editForm.planFinishTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px;">
<el-button type="primary" plain icon="Plus" size="mini" @click="addDetailRow">新增明细行</el-button>
<el-button type="danger" plain icon="Delete" size="mini" :disabled="detailSelection.length === 0" @click="removeDetailRows">删除选中</el-button>
<div style="margin-left:auto;">
<span>合计数量{{ totalQtyText }}</span>
</div>
</div>
<el-table :data="editDetails" border style="width: 100%" @selection-change="onDetailSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="行号" width="70" align="center">
<template #default="scope">
{{ scope.row.lineNo }}
</template>
</el-table-column>
<el-table-column label="物料类型" min-width="120" align="center">
<template #default="scope">
<span v-if="fixedMaterialMode">原料</span>
<el-select v-else v-model="scope.row.itemType" placeholder="请选择" @change="onItemTypeChanged(scope.row)">
<el-option label="产品" value="product" />
<el-option label="原料" value="material" />
<el-option label="其他" value="other" />
</el-select>
</template>
</el-table-column>
<el-table-column label="物料" min-width="220" align="center">
<template #default="scope">
<ProductSelect
v-if="scope.row.itemType === 'product'"
v-model="scope.row.itemId"
@change="onProductPicked(scope.row, $event)"
/>
<RawSelector
v-else-if="scope.row.itemType === 'material'"
v-model="scope.row.itemId"
:allowAdd="true"
@change="onMaterialPicked(scope.row, $event)"
/>
<el-input v-else v-model="scope.row.itemId" placeholder="请输入物料ID" />
</template>
</el-table-column>
<el-table-column label="名称快照" min-width="180" align="center">
<template #default="scope">
<el-input v-model="scope.row.itemName" placeholder="可选" />
</template>
</el-table-column>
<el-table-column label="仓库" min-width="180" align="center">
<template #default="scope">
<WarehouseSelect v-model="scope.row.warehouseId" placeholder="请选择仓库" />
</template>
</el-table-column>
<el-table-column v-if="editForm.ioType === 'T'" label="源仓库" min-width="180" align="center">
<template #default="scope">
<WarehouseSelect v-model="scope.row.fromWarehouseId" placeholder="请选择源仓库" />
</template>
</el-table-column>
<el-table-column label="数量" min-width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.quantity" placeholder="请输入数量" @input="recalcAmount(scope.row)" />
</template>
</el-table-column>
<el-table-column label="单位" width="90" align="center">
<template #default="scope">
<el-input v-model="scope.row.unit" placeholder="单位" />
</template>
</el-table-column>
<el-table-column label="批次号" min-width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.batchNo" placeholder="批次号" />
</template>
</el-table-column>
<el-table-column label="单价" min-width="110" align="center">
<template #default="scope">
<el-input v-model="scope.row.unitPrice" placeholder="单价" @input="recalcAmount(scope.row)" />
</template>
</el-table-column>
<el-table-column label="金额" min-width="110" align="center">
<template #default="scope">
<el-input v-model="scope.row.amount" placeholder="金额" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="备注" />
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button :loading="buttonLoading" type="primary" @click="submitEdit">保存</el-button>
<el-button @click="editOpen = false">取消</el-button>
</template>
</el-dialog>
<el-dialog title="单据明细" v-model="detailOpen" width="1100px" append-to-body>
<template v-if="detailData && detailData.order">
<el-descriptions :title="'单号:' + (detailData.order.orderCode || '-')" :column="2" border>
<el-descriptions-item label="类型">{{ ioTypeLabel(detailData.order.ioType) }}</el-descriptions-item>
<el-descriptions-item label="业务类型">{{ detailData.order.bizType }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.order.status === '1' ? '已完成' : '进行中' }}</el-descriptions-item>
<el-descriptions-item label="已执行">{{ detailData.order.execFlag === '1' ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="责任人">{{ detailData.order.responsibleName }}</el-descriptions-item>
<el-descriptions-item label="延迟">{{ delayStatusLabel(detailData.order.delayStatus) }}</el-descriptions-item>
</el-descriptions>
<div style="margin: 12px 0; text-align: right;">
<el-button type="primary" size="small" @click="handleAudit(detailData.order)">审核</el-button>
<el-button type="primary" size="small" @click="handleExecute(detailData.order)">执行</el-button>
<el-button type="warning" size="small" @click="handleArrival(detailData.order)">到货确认</el-button>
<el-button type="success" size="small" @click="handleFinish(detailData.order)">完成</el-button>
<el-button type="danger" size="small" @click="handleReverse(detailData.order)">冲销</el-button>
</div>
<el-table :data="detailData.details || []" border style="width: 100%">
<el-table-column label="行号" prop="lineNo" width="70" align="center" />
<el-table-column label="物料类型" prop="itemType" width="110" align="center" />
<el-table-column label="物料ID" prop="itemId" width="120" align="center" />
<el-table-column label="名称快照" prop="itemName" min-width="160" align="center" />
<el-table-column label="仓库" prop="warehouseId" width="110" align="center" />
<el-table-column label="源仓库" prop="fromWarehouseId" width="110" align="center" />
<el-table-column label="数量" prop="quantity" width="120" align="center" />
<el-table-column label="单位" prop="unit" width="90" align="center" />
<el-table-column label="批次号" prop="batchNo" min-width="120" align="center" />
<el-table-column label="备注" prop="remark" min-width="140" align="center" />
</el-table>
</template>
<template v-else>
<div style="height:200px;line-height:200px;text-align:center;">未获取到单据数据</div>
</template>
<template #footer>
<el-button @click="detailOpen = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import WarehouseSelect from '@/components/WarehouseSelect'
import ProductSelect from '@/components/ProductSelect'
import RawSelector from '@/components/RawSelector/index.vue'
import {
listStockIoOrder,
getStockIoOrderWithDetail,
addStockIoOrderWithDetail,
updateStockIoOrderWithDetail,
delStockIoOrder,
auditStockIoOrder,
executeStockIoOrder,
arrivalStockIoOrder,
finishStockIoOrder,
cancelStockIoOrder,
reverseStockIoOrder
} from '@/api/wms/stockIoOrder'
export default {
name: 'StockIoOrderPage',
components: { WarehouseSelect, ProductSelect, RawSelector },
props: {
ioType: {
type: String,
default: ''
}
},
data() {
return {
ioTypeOptions: [
{ label: '入库', value: 'I' },
{ label: '出库', value: 'O' },
{ label: '调拨', value: 'T' }
],
statusOptions: [
{ label: '进行中', value: '0' },
{ label: '已完成', value: '1' }
],
yesNoOptions: [
{ label: '否', value: '0' },
{ label: '是', value: '1' }
],
delayStatusOptions: [
{ label: '正常', value: '0' },
{ label: '超时', value: '1' },
{ label: '已处理', value: '2' }
],
loading: true,
buttonLoading: false,
ids: [],
rows: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
list: [],
queryParams: {
pageNum: 1,
pageSize: 20,
orderCode: undefined,
ioType: this.ioType || undefined,
bizType: undefined,
status: undefined,
execFlag: undefined,
reversalFlag: undefined,
delayStatus: undefined,
responsibleName: undefined,
sourceNo: undefined
},
editOpen: false,
editTitle: '',
editForm: {},
editDetails: [],
detailSelection: [],
editRules: {
ioType: [{ required: true, message: '出入库类型不能为空', trigger: 'change' }],
bizType: [{ required: true, message: '业务类型不能为空', trigger: 'blur' }]
},
detailOpen: false,
detailData: null
}
},
computed: {
showIoTypeSelector() {
return !this.ioType
},
fixedMaterialMode() {
return this.ioType === 'I' || this.ioType === 'O'
},
totalQtyText() {
const total = (this.editDetails || []).reduce((sum, r) => sum + this.toNumber(r.quantity), 0)
return String(total)
}
},
created() {
this.getList()
},
methods: {
toNumber(val) {
const n = Number(val)
return Number.isFinite(n) ? n : 0
},
ioTypeLabel(v) {
const map = { I: '入库', O: '出库', T: '调拨' }
return map[v] || v
},
ioTypeTag(v) {
if (v === 'I') return 'success'
if (v === 'O') return 'primary'
if (v === 'T') return 'warning'
return 'info'
},
delayStatusLabel(v) {
const map = { '0': '正常', '1': '超时', '2': '已处理' }
return map[v] || v
},
delayStatusTag(v) {
if (v === '1') return 'danger'
if (v === '2') return 'success'
return 'info'
},
getList() {
this.loading = true
listStockIoOrder(this.queryParams)
.then((res) => {
this.list = res.rows || []
this.total = res.total || 0
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.queryParams.ioType = this.ioType || undefined
this.handleQuery()
},
handleSelectionChange(selection) {
this.rows = selection
this.ids = selection.map((r) => r.orderId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
resetEdit() {
this.editForm = {
orderId: undefined,
orderCode: undefined,
ioType: this.ioType || 'I',
bizType: undefined,
sourceType: undefined,
sourceNo: undefined,
sourceOrderId: undefined,
responsibleId: undefined,
responsibleName: undefined,
planArrivalTime: undefined,
planFinishTime: undefined,
warehouseId: undefined,
fromWarehouseId: undefined,
toWarehouseId: undefined,
remark: undefined
}
this.editDetails = []
this.detailSelection = []
this.resetForm('editFormRef')
},
handleAdd() {
this.resetEdit()
this.editTitle = '新增出入库单据'
this.editOpen = true
this.addDetailRow()
},
handleUpdate(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) {
return
}
this.buttonLoading = true
getStockIoOrderWithDetail(target.orderId)
.then((res) => {
const data = res.data || {}
this.resetEdit()
this.editForm = Object.assign(this.editForm, data.order || {})
if (this.ioType) {
this.editForm.ioType = this.ioType
}
this.editDetails = (data.details || []).map((d) => {
const next = Object.assign({}, d)
if (this.fixedMaterialMode) {
next.itemType = 'material'
}
return next
})
if (this.editDetails.length === 0) {
this.addDetailRow()
}
this.editTitle = '修改出入库单据'
this.editOpen = true
this.rebuildLineNo()
})
.finally(() => {
this.buttonLoading = false
})
},
handleDelete(row) {
const orderIds = row && row.orderId ? row.orderId : this.ids
if (!orderIds || (Array.isArray(orderIds) && orderIds.length === 0)) {
return
}
this.$modal
.confirm('是否确认删除选中的单据?')
.then(() => {
this.loading = true
return delStockIoOrder(orderIds)
})
.then(() => {
this.$modal.msgSuccess('删除成功')
this.getList()
})
.finally(() => {
this.loading = false
})
},
showDetail(row) {
this.detailOpen = true
this.detailData = null
getStockIoOrderWithDetail(row.orderId).then((res) => {
this.detailData = res.data
})
},
handleAudit(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认审核该单据?')
.then(() => {
this.buttonLoading = true
return auditStockIoOrder(target.orderId)
})
.then(() => {
this.$modal.msgSuccess('审核成功')
this.getList()
if (this.detailOpen && this.detailData && this.detailData.order) {
return getStockIoOrderWithDetail(this.detailData.order.orderId).then((res) => (this.detailData = res.data))
}
})
.finally(() => {
this.buttonLoading = false
})
},
handleExecute(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认执行该单据?执行后将影响库存。')
.then(() => {
this.buttonLoading = true
return executeStockIoOrder(target.orderId)
})
.then(() => {
this.$modal.msgSuccess('执行成功')
this.getList()
if (this.detailOpen && this.detailData && this.detailData.order) {
return getStockIoOrderWithDetail(this.detailData.order.orderId).then((res) => (this.detailData = res.data))
}
})
.finally(() => {
this.buttonLoading = false
})
},
handleArrival(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认到货?')
.then(() => {
this.buttonLoading = true
return arrivalStockIoOrder(target.orderId)
})
.then(() => {
this.$modal.msgSuccess('已确认到货')
this.getList()
if (this.detailOpen && this.detailData && this.detailData.order) {
return getStockIoOrderWithDetail(this.detailData.order.orderId).then((res) => (this.detailData = res.data))
}
})
.finally(() => {
this.buttonLoading = false
})
},
handleFinish(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认完成该单据?')
.then(() => {
this.buttonLoading = true
return finishStockIoOrder(target.orderId)
})
.then(() => {
this.$modal.msgSuccess('已完成')
this.getList()
if (this.detailOpen && this.detailData && this.detailData.order) {
return getStockIoOrderWithDetail(this.detailData.order.orderId).then((res) => (this.detailData = res.data))
}
})
.finally(() => {
this.buttonLoading = false
})
},
handleCancel(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认作废该单据?')
.then(() => {
this.buttonLoading = true
return cancelStockIoOrder(target.orderId, '')
})
.then(() => {
this.$modal.msgSuccess('已作废')
this.getList()
this.detailOpen = false
})
.finally(() => {
this.buttonLoading = false
})
},
handleReverse(row) {
const target = row || (this.rows && this.rows[0])
if (!target || !target.orderId) return
this.$modal
.confirm('确认冲销该单据?系统将创建一张反向单据并自动执行。')
.then(() => {
this.buttonLoading = true
return reverseStockIoOrder(target.orderId, '')
})
.then((res) => {
const newId = res.data
this.$modal.msgSuccess('冲销成功冲销单ID' + newId)
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
},
submitEdit() {
this.$refs.editFormRef.validate((valid) => {
if (!valid) return
if (!this.editDetails || this.editDetails.length === 0) {
this.$modal.msgError('请先添加明细')
return
}
const payload = this.buildPayload()
this.buttonLoading = true
const req = payload.orderId ? updateStockIoOrderWithDetail(payload) : addStockIoOrderWithDetail(payload)
req
.then(() => {
this.$modal.msgSuccess('保存成功')
this.editOpen = false
this.getList()
})
.finally(() => {
this.buttonLoading = false
})
})
},
buildPayload() {
const details = (this.editDetails || [])
.filter((d) => d && (this.fixedMaterialMode || d.itemType) && d.itemId != null && String(d.itemId) !== '' && this.toNumber(d.quantity) > 0)
.map((d, idx) => {
const lineNo = d.lineNo != null ? d.lineNo : idx + 1
const unitPrice = this.toNumber(d.unitPrice)
const quantity = this.toNumber(d.quantity)
const amount = d.amount != null && String(d.amount) !== '' ? this.toNumber(d.amount) : unitPrice * quantity
return {
detailId: d.detailId,
lineNo,
itemType: this.fixedMaterialMode ? 'material' : d.itemType,
itemId: d.itemId,
itemName: d.itemName,
specName: d.specName,
warehouseId: d.warehouseId,
fromWarehouseId: d.fromWarehouseId,
quantity: quantity,
unit: d.unit,
batchNo: d.batchNo,
unitPrice: unitPrice,
amount: amount,
sourceDetailNo: d.sourceDetailNo,
reversalDetailId: d.reversalDetailId,
remark: d.remark
}
})
const totalQty = details.reduce((sum, d) => sum + this.toNumber(d.quantity), 0)
return Object.assign({}, this.editForm, { totalQty, details })
},
addDetailRow() {
const nextNo = (this.editDetails || []).length + 1
this.editDetails.push({
detailId: undefined,
lineNo: nextNo,
itemType: this.fixedMaterialMode ? 'material' : 'product',
itemId: undefined,
itemName: undefined,
specName: undefined,
warehouseId: undefined,
fromWarehouseId: undefined,
quantity: undefined,
unit: undefined,
batchNo: undefined,
unitPrice: undefined,
amount: undefined,
remark: undefined
})
if (!this.showIoTypeSelector) {
this.editForm.ioType = this.ioType
}
this.rebuildLineNo()
},
onDetailSelectionChange(rows) {
this.detailSelection = rows || []
},
removeDetailRows() {
const ids = new Set((this.detailSelection || []).map((r) => r.lineNo))
this.editDetails = (this.editDetails || []).filter((r) => !ids.has(r.lineNo))
this.detailSelection = []
if (this.editDetails.length === 0) {
this.addDetailRow()
} else {
this.rebuildLineNo()
}
},
rebuildLineNo() {
let i = 1
this.editDetails = (this.editDetails || []).map((r) => Object.assign({}, r, { lineNo: i++ }))
},
onIoTypeChanged() {
if (!this.showIoTypeSelector) {
return
}
if (this.editForm.ioType !== 'T') {
this.editForm.fromWarehouseId = undefined
this.editForm.toWarehouseId = undefined
this.editDetails = (this.editDetails || []).map((r) => Object.assign({}, r, { fromWarehouseId: undefined }))
}
},
onItemTypeChanged(row) {
if (this.fixedMaterialMode) {
row.itemType = 'material'
return
}
row.itemId = undefined
row.itemName = undefined
row.specName = undefined
row.unit = undefined
},
onProductPicked(row, product) {
if (product && product.productName) {
row.itemName = product.productName
if (!row.unit && product.unit) {
row.unit = product.unit
}
}
},
onMaterialPicked(row, material) {
if (material && material.materialName) {
row.itemName = material.materialName
if (!row.unit && material.unit) {
row.unit = material.unit
}
if (material.unitPrice != null && row.unitPrice == null) {
row.unitPrice = material.unitPrice
}
if (material.materialType != null) {
row._materialType = material.materialType
}
this.recalcAmount(row)
}
},
recalcAmount(row) {
const qty = this.toNumber(row.quantity)
const unitPrice = this.toNumber(row.unitPrice)
if (qty > 0 || unitPrice > 0) {
row.amount = String(unitPrice * qty)
}
}
}
}
</script>