Compare commits

3 Commits

2 changed files with 311 additions and 31 deletions

View File

@ -8,6 +8,7 @@ from app.models.base import MaterialBase
from app.models.inbound.buy import StockBuy from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
from app.models.inbound.stocktake import StocktakeDraft
from datetime import datetime from datetime import datetime
import random import random
import string import string
@ -221,3 +222,155 @@ def get_stocks():
'limit': limit 'limit': limit
} }
}) })
# --------------------------------------------------------
# 5. 获取盘点差异列表
# GET /api/v1/stock/adjustment/stocktake-discrepancies
# --------------------------------------------------------
@adjustment_bp.route('/stocktake-discrepancies', methods=['GET'])
@jwt_required()
@permission_required('stock_adjustment:list')
def get_stocktake_discrepancies():
"""获取所有有差异的盘点记录"""
try:
# 查询所有有差异的盘点记录
drafts = StocktakeDraft.query.filter(StocktakeDraft.diff_qty != 0).all()
items = []
for draft in drafts:
diff = float(draft.diff_qty or 0)
if diff == 0:
continue
# 获取物料基础信息
base_id = None
material_name = ''
spec_model = ''
sku = ''
warehouse_location = ''
# 根据source_table获取对应的库存记录
stock_model = get_stock_model(draft.source_table)
if stock_model and draft.stock_id:
stock = stock_model.query.get(draft.stock_id)
if stock:
base_id = getattr(stock, 'base_id', None)
sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '')
warehouse_location = getattr(stock, 'warehouse_location', '')
# 联表查询 MaterialBase
if base_id:
material = MaterialBase.query.get(base_id)
if material:
material_name = material.name
spec_model = material.spec_model
items.append({
'draft_id': draft.id,
'source_table': draft.source_table,
'stock_id': draft.stock_id,
'base_id': base_id,
'sku': sku,
'material_name': material_name,
'spec_model': spec_model,
'warehouse_location': warehouse_location,
'stock_qty': float(draft.stock_qty or 0),
'quantity': float(draft.quantity or 0),
'diff_qty': diff,
'adjust_type': 'profit' if diff > 0 else 'loss',
'remark': draft.remark or ''
})
return jsonify({
'code': 200,
'data': {
'items': items,
'total': len(items)
}
})
except Exception as e:
return jsonify({'code': 500, 'msg': f'获取盘点差异失败: {str(e)}'}), 500
# --------------------------------------------------------
# 6. 批量导入盘点差异(按需勾选)
# POST /api/v1/stock/adjustment/import-from-stocktake
# --------------------------------------------------------
@adjustment_bp.route('/import-from-stocktake', methods=['POST'])
@jwt_required()
@permission_required('stock_adjustment:operation')
def import_from_stocktake():
"""批量导入选中的盘点差异记录"""
identity = get_jwt_identity()
# 修复操作人保存为0的Bug确保保存真实的用户名
operator = identity.get('username', 'system') if isinstance(identity, dict) else str(identity)
if not operator or operator == '0':
operator = identity if identity else 'system'
data = request.get_json()
if not data or 'records' not in data:
return jsonify({'code': 400, 'msg': '缺少records参数'}), 400
records = data.get('records', [])
if not records:
return jsonify({'code': 400, 'msg': '请选择要导入的记录'}), 400
try:
count = 0
for item in records:
draft_id = item.get('draft_id')
reason = item.get('reason', '盘点差异导入')
# 获取对应的盘点记录
draft = StocktakeDraft.query.get(draft_id)
if not draft:
continue
diff = float(draft.diff_qty or 0)
if diff == 0:
continue
adjust_type = 'profit' if diff > 0 else 'loss'
adjust_quantity = abs(diff)
# 获取物料基础信息
base_id = None
sku = ''
warehouse_location = ''
stock_model = get_stock_model(draft.source_table)
if stock_model and draft.stock_id:
stock = stock_model.query.get(draft.stock_id)
if stock:
base_id = getattr(stock, 'base_id', None)
sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '')
warehouse_location = getattr(stock, 'warehouse_location', '')
# 生成调整单号
order_no = generate_order_no()
# 创建调整单
adjustment = StockAdjustment(
order_no=order_no,
base_id=base_id,
stock_id=draft.stock_id,
source_table=draft.source_table,
sku=sku,
warehouse_location=warehouse_location,
adjust_type=adjust_type,
adjust_quantity=adjust_quantity,
reason=reason,
status='pending',
operator=operator
)
db.session.add(adjustment)
count += 1
db.session.commit()
return jsonify({'code': 200, 'msg': f'成功导入 {count} 条记录', 'data': {'count': count}})
except Exception as e:
db.session.rollback()
return jsonify({'code': 500, 'msg': f'导入失败: {str(e)}'}), 500

View File

@ -13,6 +13,9 @@
<el-option label="已取消" value="cancelled" /> <el-option label="已取消" value="cancelled" />
</el-select> </el-select>
<el-button type="primary" @click="fetchData">查询</el-button> <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 v-if="userStore.hasPermission('stock_adjustment:operation')" type="success" @click="showDialog = true">
新增调整单 新增调整单
</el-button> </el-button>
@ -127,13 +130,57 @@
style="margin-top: 15px; justify-content: center" style="margin-top: 15px; justify-content: center"
/> />
</el-dialog> </el-dialog>
<!-- 盘点差异审核弹窗 -->
<el-dialog v-model="showReviewDialog" title="盘点差异审核" width="1200px" :close-on-click-modal="false">
<div v-loading="reviewLoading">
<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 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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import request from '@/utils/request'
const userStore = useUserStore() const userStore = useUserStore()
@ -178,23 +225,98 @@ const stockLimit = ref(20)
const stockKeyword = ref('') const stockKeyword = ref('')
const selectedStock = ref<any>(null) const selectedStock = ref<any>(null)
// 盘点差异审核
const showReviewDialog = ref(false)
const reviewLoading = ref(false)
const reviewList = ref<any[]>([])
const selectedReviewRows = ref<any[]>([])
const reviewTableRef = ref()
const importLoading = ref(false)
// 获取盘点差异列表
async function fetchReviewList() {
reviewLoading.value = true
try {
const res = await request({
url: '/v1/stock/adjustment/stocktake-discrepancies',
method: 'get'
})
if (res.code === 200) {
// 为每条记录设置默认原因
reviewList.value = (res.data.items || []).map((item: any) => ({
...item,
reason: item.remark || '盘点差异导入'
}))
}
} catch (e) {
ElMessage.error('获取盘点差异失败')
} finally {
reviewLoading.value = false
}
}
// 打开审核弹窗
async function openReviewDialog() {
showReviewDialog.value = true
selectedReviewRows.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() { async function fetchData() {
loading.value = true loading.value = true
try { try {
const params = new URLSearchParams({ const res = await request({
page: page.value.toString(), url: '/v1/stock/adjustment/list',
limit: limit.value.toString() method: 'get',
params: {
page: page.value,
limit: limit.value,
keyword: keyword.value || undefined,
adjust_type: searchAdjustType.value || undefined,
status: searchStatus.value || undefined
}
}) })
if (keyword.value) params.append('keyword', keyword.value) if (res.code === 200) {
if (searchAdjustType.value) params.append('adjust_type', searchAdjustType.value) list.value = res.data.items
if (searchStatus.value) params.append('status', searchStatus.value) total.value = res.data.total
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) { } catch (e) {
ElMessage.error('获取数据失败') ElMessage.error('获取数据失败')
@ -207,18 +329,19 @@ async function fetchData() {
async function fetchStocks() { async function fetchStocks() {
stockLoading.value = true stockLoading.value = true
try { try {
const params = new URLSearchParams({ const res = await request({
source_table: form.value.source_table, url: '/v1/stock/adjustment/stocks',
page: stockPage.value.toString(), method: 'get',
limit: stockLimit.value.toString() params: {
source_table: form.value.source_table,
page: stockPage.value,
limit: stockLimit.value,
keyword: stockKeyword.value || undefined
}
}) })
if (stockKeyword.value) params.append('keyword', stockKeyword.value) if (res.code === 200) {
stockList.value = res.data.items
const res = await fetch(`/api/v1/stock/adjustment/stocks?${params}`) stockTotal.value = res.data.total
const json = await res.json()
if (json.code === 200) {
stockList.value = json.data.items
stockTotal.value = json.data.total
} }
} catch (e) { } catch (e) {
ElMessage.error('获取库存失败') ElMessage.error('获取库存失败')
@ -263,13 +386,12 @@ async function submitForm() {
submitLoading.value = true submitLoading.value = true
try { try {
const res = await fetch('/api/v1/stock/adjustment/create', { const res = await request({
method: 'POST', url: '/v1/stock/adjustment/create',
headers: { 'Content-Type': 'application/json' }, method: 'post',
body: JSON.stringify(form.value) data: form.value
}) })
const json = await res.json() if (res.code === 200) {
if (json.code === 200) {
ElMessage.success('提交成功') ElMessage.success('提交成功')
showDialog.value = false showDialog.value = false
fetchData() fetchData()
@ -283,7 +405,7 @@ async function submitForm() {
} }
selectedStock.value = null selectedStock.value = null
} else { } else {
ElMessage.error(json.msg || '提交失败') ElMessage.error(res.msg || '提交失败')
} }
} catch (e) { } catch (e) {
ElMessage.error('提交失败') ElMessage.error('提交失败')
@ -292,6 +414,11 @@ async function submitForm() {
} }
} }
// 一键引入盘点差异 - 打开审核弹窗
async function handleImportStocktake() {
await openReviewDialog()
}
onMounted(() => { onMounted(() => {
fetchData() fetchData()
}) })