Files
GEAR-OA/gear-ui3/src/views/wms/stockIoOrder/out.vue
2026-05-11 17:59:12 +08:00

669 lines
25 KiB
Vue
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.

<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>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain size="mini" @click="openFlow">统计/导出</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" prop="createTime" 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" width="90">
<template #default="scope">
<el-tag v-if="scope.row.revokeFlag === '1'" type="danger">已撤回</el-tag>
<el-tag v-else type="info">正常</el-tag>
</template>
</el-table-column>
<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" :disabled="scope.row.revokeFlag === '1'" @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="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="请输入数量" />
</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="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.revokeBy || '-' }}</el-descriptions-item>
</el-descriptions>
<div style="margin: 12px 0;">
<el-table :data="operationRows(detailData.order)" border style="width: 100%">
<el-table-column label="操作" prop="action" width="120" align="center" />
<el-table-column label="操作人" prop="user" width="140" align="center" />
<el-table-column label="操作时间" prop="time" min-width="180" align="center" />
</el-table>
</div>
<div style="margin: 12px 0; text-align: right;">
<el-button type="danger" size="small" :disabled="detailData.order.revokeFlag === '1'" @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>
<el-dialog title="物料出入库统计" v-model="flowOpen" width="980px" top="6vh" append-to-body>
<el-form :inline="true" size="small" label-width="90px">
<el-form-item label="物料">
<RawSelector v-model="flowForm.itemId" :allowAdd="false" @change="onFlowMaterialChange" />
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="flowForm.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="flowLoading" @click="fetchFlow">查询</el-button>
</el-form-item>
</el-form>
<div v-loading="flowLoading" style="margin-top: 10px;">
<div ref="flowPrintContent">
<div style="font-size: 16px; font-weight: 600; text-align: center; margin-bottom: 8px;">
物料出入库统计
</div>
<div style="margin-bottom: 10px;">
<span>物料</span><span>{{ flowItemName || '-' }}</span>
<span style="margin-left: 18px;">时间</span>
<span>{{ (flowForm.timeRange && flowForm.timeRange[0]) || '-' }}</span>
<span> </span>
<span>{{ (flowForm.timeRange && flowForm.timeRange[1]) || '-' }}</span>
</div>
<el-row :gutter="10" style="margin-bottom: 10px;">
<el-col :span="6">
<el-card shadow="never">
<div>确认入库</div>
<div style="font-size: 18px; font-weight: 600;">{{ flowResult ? flowResult.confirmInQty : '-' }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div>出库</div>
<div style="font-size: 18px; font-weight: 600;">{{ flowResult ? flowResult.outQty : '-' }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div>撤回入库</div>
<div style="font-size: 18px; font-weight: 600;">{{ flowResult ? flowResult.revokeInQty : '-' }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div>撤回出库</div>
<div style="font-size: 18px; font-weight: 600;">{{ flowResult ? flowResult.revokeOutQty : '-' }}</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="10" style="margin-bottom: 10px;">
<el-col :span="6">
<el-card shadow="never">
<div>净变动</div>
<div style="font-size: 18px; font-weight: 600;">{{ flowResult ? flowResult.netQty : '-' }}</div>
</el-card>
</el-col>
</el-row>
<el-table :data="(flowResult && flowResult.rows) || []" border style="width: 100%;">
<el-table-column label="时间" prop="time" min-width="170" />
<el-table-column label="动作" prop="action" width="100" />
<el-table-column label="单号" prop="orderCode" min-width="160" />
<el-table-column label="数量变化" prop="qtyChange" width="120" />
</el-table>
</div>
</div>
<template #footer>
<el-button :disabled="!flowResult" @click="exportFlow">导出Excel</el-button>
<el-button @click="flowOpen = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import RawSelector from '@/components/RawSelector/index.vue'
import {
listStockIoOrder,
getStockIoOrderWithDetail,
addStockIoOrderWithDetail,
revokeStockIoOrder,
getMaterialFlow
} 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,
flowOpen: false,
flowLoading: false,
flowForm: {
itemId: undefined,
timeRange: []
},
flowItemName: '',
flowResult: 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
},
operationRows(order) {
const rows = []
if (!order) return rows
rows.push({
action: '创建单据',
user: order.createBy || '-',
time: order.createTime || '-'
})
if (order.executeTime) {
rows.push({
action: '出库',
user: order.executeBy || '-',
time: order.executeTime || '-'
})
}
if (order.revokeTime) {
rows.push({
action: '撤回',
user: order.revokeBy || '-',
time: order.revokeTime || '-'
})
}
return rows
},
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
},
handleExport() {
this.download(
'/gear/stockIoOrder/export',
{
...this.queryParams
},
`stockIoOrder_out_${new Date().getTime()}.xlsx`
)
},
resetEdit() {
this.editForm = {
ioType: 'O',
bizType: 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 revokeStockIoOrder(target.orderId, '')
})
.then(() => {
this.$modal.msgSuccess('撤回成功')
this.getList()
if (this.detailOpen) {
this.detailOpen = false
}
})
.finally(() => {
this.buttonLoading = false
})
},
openFlow() {
this.flowOpen = true
this.flowResult = null
if (!this.flowForm.timeRange || this.flowForm.timeRange.length !== 2) {
const end = new Date()
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000)
this.flowForm.timeRange = [this.formatDateTime(start), this.formatDateTime(end)]
}
},
formatDateTime(d) {
const pad = (n) => (n < 10 ? '0' + n : String(n))
const yyyy = d.getFullYear()
const MM = pad(d.getMonth() + 1)
const dd = pad(d.getDate())
const HH = pad(d.getHours())
const mm = pad(d.getMinutes())
const ss = pad(d.getSeconds())
return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`
},
onFlowMaterialChange(material) {
this.flowItemName = material && material.materialName ? material.materialName : ''
},
fetchFlow() {
if (!this.flowForm.itemId) {
this.$modal.msgError('请选择物料')
return
}
if (!this.flowForm.timeRange || this.flowForm.timeRange.length !== 2) {
this.$modal.msgError('请选择时间范围')
return
}
this.flowLoading = true
this.flowResult = null
getMaterialFlow({
itemId: this.flowForm.itemId,
startTime: this.flowForm.timeRange[0],
endTime: this.flowForm.timeRange[1]
})
.then((res) => {
this.flowResult = res.data || null
})
.finally(() => {
this.flowLoading = false
})
},
exportFlow() {
if (!this.flowForm.itemId) {
this.$modal.msgError('请选择物料')
return
}
if (!this.flowForm.timeRange || this.flowForm.timeRange.length !== 2) {
this.$modal.msgError('请选择时间范围')
return
}
this.download(
'/gear/stockIoOrder/materialFlow/export',
{
itemId: this.flowForm.itemId,
startTime: this.flowForm.timeRange[0],
endTime: this.flowForm.timeRange[1]
},
`material_flow_${this.flowForm.itemId}_${new Date().getTime()}.xlsx`
)
},
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.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 quantity = this.toNumber(d.quantity)
return {
detailId: d.detailId,
lineNo,
itemType: 'material',
itemId: d.itemId,
materialTypeSnapshot: d._materialType,
itemName: d.itemName,
quantity: quantity,
unit: d.unit,
batchNo: d.batchNo,
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,
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) === '')
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 (material.materialType != null) {
row._materialType = material.materialType
}
if (!row.unit && material.unit) {
row.unit = material.unit
}
}
},
}
}
</script>