603 lines
20 KiB
Vue
603 lines
20 KiB
Vue
<template>
|
|
<div class="app-container">
|
|
<!-- 筛选条件 -->
|
|
<div class="filter-container">
|
|
<el-input v-model="keyword" placeholder="搜索单号/SKU/物料名称" style="width: 220px" @keyup.enter="fetchData" clearable />
|
|
<el-select v-model="searchAdjustType" placeholder="调整类型" style="width: 120px" clearable>
|
|
<el-option label="盘盈" value="profit" />
|
|
<el-option label="盘亏" value="loss" />
|
|
</el-select>
|
|
<el-select v-model="searchStatus" placeholder="状态" style="width: 120px" clearable>
|
|
<el-option label="待处理" value="pending" />
|
|
<el-option label="已完成" value="completed" />
|
|
<el-option label="已取消" value="cancelled" />
|
|
</el-select>
|
|
<el-button type="primary" @click="fetchData">查询</el-button>
|
|
<el-button v-if="userStore.hasPermission('stock_adjustment:operation')" type="warning" @click="handleImportStocktake">
|
|
一键引入盘点差异
|
|
</el-button>
|
|
<el-button v-if="userStore.hasPermission('stock_adjustment:operation')" type="success" @click="showDialog = true">
|
|
新增调整单
|
|
</el-button>
|
|
</div>
|
|
|
|
<!-- 数据表格 -->
|
|
<el-table :data="list" border stripe v-loading="loading" style="margin-top: 20px">
|
|
<el-table-column prop="order_no" label="单号" width="180" />
|
|
<el-table-column prop="sku" label="SKU" width="140" />
|
|
<el-table-column prop="material_name" label="物料名称" min-width="150" show-overflow-tooltip />
|
|
<el-table-column prop="spec_model" label="规格型号" width="120" show-overflow-tooltip />
|
|
<el-table-column prop="warehouse_location" label="库位" width="100" />
|
|
<el-table-column label="调整类型" width="90" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.adjust_type === 'profit' ? 'success' : 'danger'">
|
|
{{ row.adjust_type === 'profit' ? '盘盈' : '盘亏' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="adjust_quantity" label="调整数量" width="100" align="center">
|
|
<template #default="{ row }">
|
|
<span :style="{ color: row.adjust_type === 'profit' ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
|
|
{{ row.adjust_type === 'profit' ? '+' : '-' }}{{ row.adjust_quantity }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="reason" label="调整原因" min-width="150" show-overflow-tooltip />
|
|
<el-table-column prop="linked_sku" label="关联SKU" width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
<span v-if="row.adjust_type === 'profit'">{{ row.linked_sku || '-' }}</span>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="linked_outbound_no" label="关联出库单" width="140" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
<span v-if="row.adjust_type === 'loss'">{{ row.linked_outbound_no || '-' }}</span>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="operator" label="操作人" width="100" />
|
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag v-if="row.status === 'completed'" type="success" size="small">已完成</el-tag>
|
|
<el-tag v-else-if="row.status === 'pending'" type="warning" size="small">待处理</el-tag>
|
|
<el-tag v-else type="info" size="small">已取消</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="create_time" label="创建时间" width="160" />
|
|
<el-table-column label="操作" width="100" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button v-if="row.status === 'pending' && userStore.hasPermission('stock_adjustment:operation')" type="primary" link @click="openProcessDialog(row)">
|
|
处理
|
|
</el-button>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 分页 -->
|
|
<el-pagination
|
|
background
|
|
layout="prev, pager, next, total"
|
|
:total="total"
|
|
:page-size="limit"
|
|
v-model:current-page="page"
|
|
@current-change="fetchData"
|
|
style="margin-top: 20px; justify-content: center"
|
|
/>
|
|
|
|
<!-- 新增调整单弹窗 -->
|
|
<el-dialog v-model="showDialog" title="新增盘盈盘亏调整单" width="700px" :close-on-click-modal="false">
|
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
|
<el-form-item label="选择物料" prop="stock_id">
|
|
<el-select
|
|
v-model="form.source_table"
|
|
placeholder="库存类型"
|
|
style="width: 150px; margin-right: 10px"
|
|
@change="handleSourceTableChange"
|
|
>
|
|
<el-option label="采购库存" value="stock_buy" />
|
|
<el-option label="半成品库存" value="stock_semi" />
|
|
<el-option label="成品库存" value="stock_product" />
|
|
</el-select>
|
|
<el-button @click="openStockSelector" type="primary" plain>选择物料</el-button>
|
|
<span v-if="selectedStock" style="margin-left: 10px">
|
|
{{ selectedStock.sku }} - {{ selectedStock.material_name }} (库存: {{ selectedStock.stock_quantity }})
|
|
</span>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="调整类型" prop="adjust_type">
|
|
<el-radio-group v-model="form.adjust_type">
|
|
<el-radio label="profit">盘盈 (加库存)</el-radio>
|
|
<el-radio label="loss">盘亏 (减库存)</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="调整数量" prop="adjust_quantity">
|
|
<el-input-number v-model="form.adjust_quantity" :min="1" :max="form.adjust_type === 'loss' ? (selectedStock?.stock_quantity || 9999) : 99999" style="width: 200px" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="调整原因" prop="reason">
|
|
<el-input v-model="form.reason" type="textarea" :rows="3" placeholder="请输入调整原因(必填)" />
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="showDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="submitForm" :loading="submitLoading">提交</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 物料选择弹窗 -->
|
|
<el-dialog v-model="showStockDialog" title="选择物料" width="800px">
|
|
<div class="filter-container" style="margin-bottom: 15px">
|
|
<el-input v-model="stockKeyword" placeholder="搜索SKU/条码" style="width: 200px" @keyup.enter="fetchStocks" clearable />
|
|
<el-button type="primary" @click="fetchStocks">搜索</el-button>
|
|
</div>
|
|
<el-table :data="stockList" border stripe v-loading="stockLoading" @row-click="selectStock" highlight-current-row style="cursor: pointer">
|
|
<el-table-column prop="sku" label="SKU" width="140" />
|
|
<el-table-column prop="material_name" label="物料名称" min-width="150" />
|
|
<el-table-column prop="spec_model" label="规格型号" width="120" />
|
|
<el-table-column prop="stock_quantity" label="当前库存" width="100" align="center" />
|
|
<el-table-column prop="warehouse_location" label="库位" width="100" />
|
|
</el-table>
|
|
<el-pagination
|
|
background
|
|
layout="prev, pager, next"
|
|
:total="stockTotal"
|
|
:page-size="stockLimit"
|
|
v-model:current-page="stockPage"
|
|
@current-change="fetchStocks"
|
|
style="margin-top: 15px; justify-content: center"
|
|
/>
|
|
</el-dialog>
|
|
|
|
<!-- 盘点差异审核弹窗 -->
|
|
<el-dialog v-model="showReviewDialog" title="盘点差异审核" width="1200px" :close-on-click-modal="false">
|
|
<div v-loading="reviewLoading">
|
|
<!-- 搜索栏 -->
|
|
<div style="margin-bottom: 16px; display: flex; gap: 12px;">
|
|
<el-input
|
|
v-model="reviewKeyword"
|
|
placeholder="搜索 SKU..."
|
|
clearable
|
|
style="width: 240px"
|
|
@keyup.enter="handleReviewSearch"
|
|
@clear="handleReviewSearch"
|
|
>
|
|
<template #prefix>
|
|
<el-icon><Search /></el-icon>
|
|
</template>
|
|
</el-input>
|
|
<el-button type="primary" @click="handleReviewSearch">搜索</el-button>
|
|
</div>
|
|
|
|
<el-table :data="reviewList" border stripe ref="reviewTableRef" @selection-change="handleReviewSelectionChange">
|
|
<el-table-column type="selection" width="50" />
|
|
<el-table-column prop="sku" label="SKU" width="140" />
|
|
<el-table-column prop="material_name" label="物料名称" min-width="150" show-overflow-tooltip />
|
|
<el-table-column prop="spec_model" label="规格" width="120" show-overflow-tooltip />
|
|
<el-table-column prop="warehouse_location" label="库位" width="100" />
|
|
<el-table-column prop="stock_qty" label="账面数" width="80" align="center" />
|
|
<el-table-column prop="quantity" label="实盘数" width="80" align="center" />
|
|
<el-table-column prop="diff_qty" label="差异数" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<span :style="{ color: row.diff_qty > 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
|
|
{{ row.diff_qty > 0 ? '+' : '' }}{{ row.diff_qty }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="adjust_type" label="类型" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.adjust_type === 'profit' ? 'success' : 'danger'" size="small">
|
|
{{ row.adjust_type === 'profit' ? '盘盈' : '盘亏' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="reason" label="调整原因" min-width="180">
|
|
<template #default="{ row }">
|
|
<el-input v-model="row.reason" placeholder="请输入调整原因" size="small" />
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 分页 -->
|
|
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
|
<el-pagination
|
|
v-model:current-page="reviewPage"
|
|
v-model:page-size="reviewLimit"
|
|
:page-sizes="[20, 50, 100, 200]"
|
|
:total="reviewTotal"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
@size-change="handleReviewLimitChange"
|
|
@current-change="handleReviewPageChange"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="reviewList.length === 0" style="text-align: center; padding: 40px; color: #909399">
|
|
暂无盘点差异记录
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<el-button @click="showReviewDialog = false">取消</el-button>
|
|
<el-button type="primary" :disabled="selectedReviewRows.length === 0" @click="handleImportSelected" :loading="importLoading">
|
|
勾选导入 ({{ selectedReviewRows.length }})
|
|
</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 处理调整单弹窗 -->
|
|
<el-dialog v-model="showProcessDialog" title="处理调整单" width="500px" :close-on-click-modal="false">
|
|
<el-form label-width="120px">
|
|
<el-form-item label="调整单号">
|
|
<el-input v-model="processForm.order_no" disabled />
|
|
</el-form-item>
|
|
<el-form-item label="调整类型">
|
|
<el-tag :type="processForm.adjust_type === 'profit' ? 'success' : 'danger'">
|
|
{{ processForm.adjust_type === 'profit' ? '盘盈' : '盘亏' }}
|
|
</el-tag>
|
|
</el-form-item>
|
|
<el-form-item :label="processForm.adjust_type === 'profit' ? '关联入库SKU' : '关联出库单号'">
|
|
<el-input
|
|
v-model="processForm.linked_value"
|
|
:placeholder="processForm.adjust_type === 'profit' ? '请输入已入库的SKU' : '请输入对应的出库单号'"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showProcessDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="handleProcessSubmit" :loading="processLoading">确认处理</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { Search } from '@element-plus/icons-vue'
|
|
import { useUserStore } from '@/stores/user'
|
|
import request from '@/utils/request'
|
|
|
|
const userStore = useUserStore()
|
|
|
|
// 列表数据
|
|
const loading = ref(false)
|
|
const list = ref([])
|
|
const total = ref(0)
|
|
const page = ref(1)
|
|
const limit = ref(10)
|
|
|
|
// 筛选条件
|
|
const keyword = ref('')
|
|
const searchAdjustType = ref('')
|
|
const searchStatus = ref('')
|
|
|
|
// 新增表单
|
|
const showDialog = ref(false)
|
|
const submitLoading = ref(false)
|
|
const formRef = ref()
|
|
const form = ref({
|
|
source_table: 'stock_buy',
|
|
stock_id: null,
|
|
adjust_type: 'profit',
|
|
adjust_quantity: 1,
|
|
reason: ''
|
|
})
|
|
|
|
const rules = {
|
|
stock_id: [{ required: true, message: '请选择物料', trigger: 'change' }],
|
|
adjust_type: [{ required: true, message: '请选择调整类型', trigger: 'change' }],
|
|
adjust_quantity: [{ required: true, message: '请输入调整数量', trigger: 'blur' }],
|
|
reason: [{ required: true, message: '请输入调整原因', trigger: 'blur' }]
|
|
}
|
|
|
|
// 物料选择
|
|
const showStockDialog = ref(false)
|
|
const stockLoading = ref(false)
|
|
const stockList = ref([])
|
|
const stockTotal = ref(0)
|
|
const stockPage = ref(1)
|
|
const stockLimit = ref(20)
|
|
const stockKeyword = ref('')
|
|
const selectedStock = ref<any>(null)
|
|
|
|
// 盘点差异审核
|
|
const showReviewDialog = ref(false)
|
|
const reviewLoading = ref(false)
|
|
const reviewList = ref<any[]>([])
|
|
const reviewTotal = ref(0)
|
|
const reviewPage = ref(1)
|
|
const reviewLimit = ref(20)
|
|
const reviewKeyword = ref('')
|
|
const selectedReviewRows = ref<any[]>([])
|
|
const reviewTableRef = ref()
|
|
const importLoading = ref(false)
|
|
|
|
// 处理调整单
|
|
const showProcessDialog = ref(false)
|
|
const processLoading = ref(false)
|
|
const processForm = ref({
|
|
id: null as number | null,
|
|
order_no: '',
|
|
adjust_type: '',
|
|
linked_value: ''
|
|
})
|
|
|
|
// 打开处理弹窗
|
|
function openProcessDialog(row: any) {
|
|
processForm.value = {
|
|
id: row.id,
|
|
order_no: row.order_no,
|
|
adjust_type: row.adjust_type,
|
|
linked_value: ''
|
|
}
|
|
showProcessDialog.value = true
|
|
}
|
|
|
|
// 提交处理
|
|
async function handleProcessSubmit() {
|
|
if (!processForm.value.id) return
|
|
|
|
if (processForm.value.adjust_type === 'profit' && !processForm.value.linked_value) {
|
|
ElMessage.warning('请输入已入库的SKU')
|
|
return
|
|
}
|
|
if (processForm.value.adjust_type === 'loss' && !processForm.value.linked_value) {
|
|
ElMessage.warning('请输入对应的出库单号')
|
|
return
|
|
}
|
|
|
|
processLoading.value = true
|
|
try {
|
|
const data: any = {}
|
|
if (processForm.value.adjust_type === 'profit') {
|
|
data.linked_sku = processForm.value.linked_value
|
|
} else {
|
|
data.linked_outbound_no = processForm.value.linked_value
|
|
}
|
|
|
|
const res = await request({
|
|
url: `/v1/stock/adjustment/${processForm.value.id}/process`,
|
|
method: 'post',
|
|
data
|
|
})
|
|
if (res.code === 200) {
|
|
ElMessage.success('处理成功')
|
|
showProcessDialog.value = false
|
|
fetchData()
|
|
} else {
|
|
ElMessage.error(res.msg || '处理失败')
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('处理失败')
|
|
} finally {
|
|
processLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 获取盘点差异列表
|
|
async function fetchReviewList() {
|
|
reviewLoading.value = true
|
|
try {
|
|
const res = await request({
|
|
url: '/v1/stock/adjustment/stocktake-discrepancies',
|
|
method: 'get',
|
|
params: {
|
|
page: reviewPage.value,
|
|
limit: reviewLimit.value,
|
|
keyword: reviewKeyword.value
|
|
}
|
|
})
|
|
if (res.code === 200) {
|
|
// 为每条记录设置默认原因
|
|
reviewList.value = (res.data.items || []).map((item: any) => ({
|
|
...item,
|
|
reason: item.remark || '盘点差异导入'
|
|
}))
|
|
reviewTotal.value = res.data.total || 0
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('获取盘点差异失败')
|
|
} finally {
|
|
reviewLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 搜索 SKU
|
|
function handleReviewSearch() {
|
|
reviewPage.value = 1
|
|
fetchReviewList()
|
|
}
|
|
|
|
// 分页变化
|
|
function handleReviewPageChange(page: number) {
|
|
reviewPage.value = page
|
|
fetchReviewList()
|
|
}
|
|
|
|
function handleReviewLimitChange(limit: number) {
|
|
reviewLimit.value = limit
|
|
reviewPage.value = 1
|
|
fetchReviewList()
|
|
}
|
|
|
|
// 打开审核弹窗
|
|
async function openReviewDialog() {
|
|
showReviewDialog.value = true
|
|
selectedReviewRows.value = []
|
|
reviewPage.value = 1
|
|
reviewKeyword.value = ''
|
|
await fetchReviewList()
|
|
}
|
|
|
|
// 勾选变化
|
|
function handleReviewSelectionChange(rows: any[]) {
|
|
selectedReviewRows.value = rows
|
|
}
|
|
|
|
// 导入选中的记录
|
|
async function handleImportSelected() {
|
|
if (selectedReviewRows.value.length === 0) {
|
|
ElMessage.warning('请选择要导入的记录')
|
|
return
|
|
}
|
|
|
|
importLoading.value = true
|
|
try {
|
|
const records = selectedReviewRows.value.map(row => ({
|
|
draft_id: row.draft_id,
|
|
reason: row.reason || '盘点差异导入'
|
|
}))
|
|
const res = await request({
|
|
url: '/v1/stock/adjustment/import-from-stocktake',
|
|
method: 'post',
|
|
data: { records }
|
|
})
|
|
if (res.code === 200) {
|
|
ElMessage.success(res.msg || '导入成功')
|
|
showReviewDialog.value = false
|
|
fetchData()
|
|
} else {
|
|
ElMessage.error(res.msg || '导入失败')
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('导入失败')
|
|
} finally {
|
|
importLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 获取列表
|
|
async function fetchData() {
|
|
loading.value = true
|
|
try {
|
|
const res = await request({
|
|
url: '/v1/stock/adjustment/list',
|
|
method: 'get',
|
|
params: {
|
|
page: page.value,
|
|
limit: limit.value,
|
|
keyword: keyword.value || undefined,
|
|
adjust_type: searchAdjustType.value || undefined,
|
|
status: searchStatus.value || undefined
|
|
}
|
|
})
|
|
if (res.code === 200) {
|
|
list.value = res.data.items
|
|
total.value = res.data.total
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('获取数据失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 获取库存列表
|
|
async function fetchStocks() {
|
|
stockLoading.value = true
|
|
try {
|
|
const res = await request({
|
|
url: '/v1/stock/adjustment/stocks',
|
|
method: 'get',
|
|
params: {
|
|
source_table: form.value.source_table,
|
|
page: stockPage.value,
|
|
limit: stockLimit.value,
|
|
keyword: stockKeyword.value || undefined
|
|
}
|
|
})
|
|
if (res.code === 200) {
|
|
stockList.value = res.data.items
|
|
stockTotal.value = res.data.total
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('获取库存失败')
|
|
} finally {
|
|
stockLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 打开物料选择
|
|
function openStockSelector() {
|
|
if (!form.value.source_table) {
|
|
ElMessage.warning('请先选择库存类型')
|
|
return
|
|
}
|
|
stockKeyword.value = ''
|
|
stockPage.value = 1
|
|
fetchStocks()
|
|
showStockDialog.value = true
|
|
}
|
|
|
|
// 选择物料
|
|
function selectStock(row: any) {
|
|
selectedStock.value = row
|
|
form.value.stock_id = row.stock_id
|
|
showStockDialog.value = false
|
|
// 盘亏时自动限制最大数量
|
|
if (form.value.adjust_type === 'loss') {
|
|
form.value.adjust_quantity = Math.min(form.value.adjust_quantity, row.stock_quantity)
|
|
}
|
|
}
|
|
|
|
// 库存类型变化时清空选择
|
|
function handleSourceTableChange() {
|
|
selectedStock.value = null
|
|
form.value.stock_id = null
|
|
}
|
|
|
|
// 提交表单
|
|
async function submitForm() {
|
|
if (!formRef.value) return
|
|
await formRef.value.validate()
|
|
|
|
submitLoading.value = true
|
|
try {
|
|
const res = await request({
|
|
url: '/v1/stock/adjustment/create',
|
|
method: 'post',
|
|
data: form.value
|
|
})
|
|
if (res.code === 200) {
|
|
ElMessage.success('提交成功')
|
|
showDialog.value = false
|
|
fetchData()
|
|
// 重置表单
|
|
form.value = {
|
|
source_table: 'stock_buy',
|
|
stock_id: null,
|
|
adjust_type: 'profit',
|
|
adjust_quantity: 1,
|
|
reason: ''
|
|
}
|
|
selectedStock.value = null
|
|
} else {
|
|
ElMessage.error(res.msg || '提交失败')
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('提交失败')
|
|
} finally {
|
|
submitLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 一键引入盘点差异 - 打开审核弹窗
|
|
async function handleImportStocktake() {
|
|
await openReviewDialog()
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchData()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.filter-container {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
</style>
|