Compare commits
3 Commits
5454947176
...
ae63748060
| Author | SHA1 | Date | |
|---|---|---|---|
| ae63748060 | |||
| 05410b5b4d | |||
| 50cf63414f |
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user