feat: initialize inventory profit and loss adjustment module

This commit is contained in:
DXC
2026-03-19 12:06:32 +08:00
parent 7a4717ce21
commit d8a57ab66e
6 changed files with 634 additions and 1 deletions

View File

@ -0,0 +1,307 @@
<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="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="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>
<!-- 分页 -->
<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>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/user'
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)
// 获取列表
async function fetchData() {
loading.value = true
try {
const params = new URLSearchParams({
page: page.value.toString(),
limit: limit.value.toString()
})
if (keyword.value) params.append('keyword', keyword.value)
if (searchAdjustType.value) params.append('adjust_type', searchAdjustType.value)
if (searchStatus.value) params.append('status', searchStatus.value)
const res = await fetch(`/api/v1/stock/adjustment/list?${params}`)
const json = await res.json()
if (json.code === 200) {
list.value = json.data.items
total.value = json.data.total
}
} catch (e) {
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
// 获取库存列表
async function fetchStocks() {
stockLoading.value = true
try {
const params = new URLSearchParams({
source_table: form.value.source_table,
page: stockPage.value.toString(),
limit: stockLimit.value.toString()
})
if (stockKeyword.value) params.append('keyword', stockKeyword.value)
const res = await fetch(`/api/v1/stock/adjustment/stocks?${params}`)
const json = await res.json()
if (json.code === 200) {
stockList.value = json.data.items
stockTotal.value = json.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 fetch('/api/v1/stock/adjustment/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value)
})
const json = await res.json()
if (json.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(json.msg || '提交失败')
}
} catch (e) {
ElMessage.error('提交失败')
} finally {
submitLoading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.filter-container {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
</style>