Compare commits

2 Commits

5 changed files with 269 additions and 56 deletions

View File

@ -50,6 +50,17 @@ try:
except ImportError: except ImportError:
StockProduct = None StockProduct = None
def get_stock_model(source_table):
"""根据source_table获取对应的库存模型"""
if source_table == 'stock_buy':
return StockBuy
elif source_table == 'stock_semi':
return StockSemi
elif source_table == 'stock_product':
return StockProduct
return None
from app.services.print.network_print_service import NetworkPrintService from app.services.print.network_print_service import NetworkPrintService
bp = Blueprint('stock_ops', __name__) bp = Blueprint('stock_ops', __name__)
@ -145,9 +156,13 @@ def get_all_stock():
@permission_required('inventory_stocktake') @permission_required('inventory_stocktake')
def get_drafts(): def get_drafts():
""" """
获取当前用户的盘点进度 获取盘点草稿列表
支持过滤: session_id 支持分页、搜索SKU和排序
""" """
# 获取分页参数
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 20, type=int)
keyword = request.args.get('keyword', '', type=str)
session_id = request.args.get('session_id') session_id = request.args.get('session_id')
query = StocktakeDraft.query query = StocktakeDraft.query
@ -155,8 +170,58 @@ def get_drafts():
if session_id: if session_id:
query = query.filter_by(session_id=session_id) query = query.filter_by(session_id=session_id)
drafts = query.order_by(StocktakeDraft.scan_time.desc()).all() # 先执行查询获取所有记录
return jsonify([d.to_dict() for d in drafts]), 200 drafts = query.all()
items = []
for draft in drafts:
# 获取 SKU 信息
sku = ''
material_name = ''
spec_model = ''
# 根据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:
sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '')
# 如果有关键词,进行 SKU 模糊匹配
if keyword and sku:
if keyword.lower() not in sku.lower():
continue
# 获取物料基础信息
base_id = getattr(stock, 'base_id', None)
if base_id:
material = MaterialBase.query.get(base_id)
if material:
material_name = material.name
spec_model = material.spec_model
item = draft.to_dict()
item['sku'] = sku
item['material_name'] = material_name
item['spec_model'] = spec_model
items.append(item)
# 按 SKU 升序排序
items.sort(key=lambda x: (x['sku'] or '').lower())
# 手动分页
total = len(items)
start = (page - 1) * limit
end = start + limit
paginated_items = items[start:end]
return jsonify({
'items': paginated_items,
'total': total,
'page': page,
'limit': limit
}), 200
@bp.route('/draft/add', methods=['POST']) @bp.route('/draft/add', methods=['POST'])

View File

@ -256,12 +256,21 @@ def get_stocks():
@jwt_required() @jwt_required()
@permission_required('stock_adjustment:list') @permission_required('stock_adjustment:list')
def get_stocktake_discrepancies(): def get_stocktake_discrepancies():
"""获取所有有差异的盘点记录""" """获取所有有差异的盘点记录,支持分页、搜索和排序"""
try: try:
# 查询所有有差异的盘点记录 # 获取分页参数
drafts = StocktakeDraft.query.filter(StocktakeDraft.diff_qty != 0).all() page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 20, type=int)
keyword = request.args.get('keyword', '', type=str)
# 先查询有差异的记录
query = StocktakeDraft.query.filter(StocktakeDraft.diff_qty != 0)
# 执行查询(不排序,因为在 Python 中排序)
drafts = query.all()
items = [] items = []
for draft in drafts: for draft in drafts:
diff = float(draft.diff_qty or 0) diff = float(draft.diff_qty or 0)
if diff == 0: if diff == 0:
@ -283,6 +292,11 @@ def get_stocktake_discrepancies():
sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '') sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '')
warehouse_location = getattr(stock, 'warehouse_location', '') warehouse_location = getattr(stock, 'warehouse_location', '')
# 如果有关键词,进行 SKU 模糊匹配
if keyword and sku:
if keyword.lower() not in sku.lower():
continue
# 联表查询 MaterialBase # 联表查询 MaterialBase
if base_id: if base_id:
material = MaterialBase.query.get(base_id) material = MaterialBase.query.get(base_id)
@ -306,11 +320,22 @@ def get_stocktake_discrepancies():
'remark': draft.remark or '' 'remark': draft.remark or ''
}) })
# 按 SKU 升序排序
items.sort(key=lambda x: (x['sku'] or '').lower())
# 手动分页
total = len(items)
start = (page - 1) * limit
end = start + limit
paginated_items = items[start:end]
return jsonify({ return jsonify({
'code': 200, 'code': 200,
'data': { 'data': {
'items': items, 'items': paginated_items,
'total': len(items) 'total': total,
'page': page,
'limit': limit
} }
}) })

View File

@ -92,20 +92,28 @@ const routes: Array<RouteRecordRaw> = [
name: 'InventorySummary', name: 'InventorySummary',
component: () => import('@/views/stock/inbound/inbound_summary.vue'), component: () => import('@/views/stock/inbound/inbound_summary.vue'),
meta: { title: '入库记录' } meta: { title: '入库记录' }
}
]
}, },
// ★ [新增] 库存盘点页面 (查库/消除)
// 4.1 盘点管理 (独立顶级菜单)
{ {
path: 'stocktake', path: '/stocktake',
component: Layout,
redirect: '/stocktake/operation',
meta: { title: '盘点管理', icon: 'DataBoard' },
children: [
{
path: 'operation',
name: 'InventoryStocktake', name: 'InventoryStocktake',
component: () => import('@/views/stock/stocktake/index.vue'), component: () => import('@/views/stock/stocktake/index.vue'),
meta: { title: '库存盘点' } meta: { title: '盲盘作业' }
}, },
// ★ [新增] 盘盈盘亏管理页面
{ {
path: 'adjustment', path: 'adjustment',
name: 'StockAdjustment', name: 'StockAdjustment',
component: () => import('@/views/stock/adjustment/index.vue'), component: () => import('@/views/stock/adjustment/index.vue'),
meta: { title: '盘盈盘亏管理' } meta: { title: '盈亏调整' }
} }
] ]
}, },

View File

@ -154,6 +154,23 @@
<!-- 盘点差异审核弹窗 --> <!-- 盘点差异审核弹窗 -->
<el-dialog v-model="showReviewDialog" title="盘点差异审核" width="1200px" :close-on-click-modal="false"> <el-dialog v-model="showReviewDialog" title="盘点差异审核" width="1200px" :close-on-click-modal="false">
<div v-loading="reviewLoading"> <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 :data="reviewList" border stripe ref="reviewTableRef" @selection-change="handleReviewSelectionChange">
<el-table-column type="selection" width="50" /> <el-table-column type="selection" width="50" />
<el-table-column prop="sku" label="SKU" width="140" /> <el-table-column prop="sku" label="SKU" width="140" />
@ -182,6 +199,20 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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 v-if="reviewList.length === 0" style="text-align: center; padding: 40px; color: #909399">
暂无盘点差异记录 暂无盘点差异记录
</div> </div>
@ -223,6 +254,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import request from '@/utils/request' import request from '@/utils/request'
@ -273,6 +305,10 @@ const selectedStock = ref<any>(null)
const showReviewDialog = ref(false) const showReviewDialog = ref(false)
const reviewLoading = ref(false) const reviewLoading = ref(false)
const reviewList = ref<any[]>([]) const reviewList = ref<any[]>([])
const reviewTotal = ref(0)
const reviewPage = ref(1)
const reviewLimit = ref(20)
const reviewKeyword = ref('')
const selectedReviewRows = ref<any[]>([]) const selectedReviewRows = ref<any[]>([])
const reviewTableRef = ref() const reviewTableRef = ref()
const importLoading = ref(false) const importLoading = ref(false)
@ -345,7 +381,12 @@ async function fetchReviewList() {
try { try {
const res = await request({ const res = await request({
url: '/v1/stock/adjustment/stocktake-discrepancies', url: '/v1/stock/adjustment/stocktake-discrepancies',
method: 'get' method: 'get',
params: {
page: reviewPage.value,
limit: reviewLimit.value,
keyword: reviewKeyword.value
}
}) })
if (res.code === 200) { if (res.code === 200) {
// 为每条记录设置默认原因 // 为每条记录设置默认原因
@ -353,6 +394,7 @@ async function fetchReviewList() {
...item, ...item,
reason: item.remark || '盘点差异导入' reason: item.remark || '盘点差异导入'
})) }))
reviewTotal.value = res.data.total || 0
} }
} catch (e) { } catch (e) {
ElMessage.error('获取盘点差异失败') ElMessage.error('获取盘点差异失败')
@ -361,10 +403,30 @@ async function fetchReviewList() {
} }
} }
// 搜索 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() { async function openReviewDialog() {
showReviewDialog.value = true showReviewDialog.value = true
selectedReviewRows.value = [] selectedReviewRows.value = []
reviewPage.value = 1
reviewKeyword.value = ''
await fetchReviewList() await fetchReviewList()
} }

View File

@ -211,57 +211,50 @@
destroy-on-close destroy-on-close
class="inventory-drawer" class="inventory-drawer"
> >
<div class="drawer-layout"> <div class="drawer-layout" v-loading="listLoading">
<div class="search-bar"> <div class="search-bar">
<el-input v-model="searchKeyword" placeholder="搜索 SKU/名称..." :prefix-icon="Search" clearable /> <el-input v-model="listKeyword" placeholder="搜索 SKU..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" />
<el-select v-model="filterType" placeholder="状态" style="width: 100px; margin-left: 8px;"> <el-button type="primary" @click="handleListSearch">搜索</el-button>
<el-option label="全部" value="all" />
<el-option label="已盘" value="scanned" />
<el-option label="未盘" value="missing" />
</el-select>
</div> </div>
<div class="table-container"> <div class="table-container">
<el-table <el-table
:data="filteredList" :data="listData"
height="100%" height="100%"
stripe stripe
border border
row-key="uniqueKey" row-key="id"
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="name" label="名称" min-width="90" show-overflow-tooltip /> <el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
<el-table-column prop="material_name" label="名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="sku" label="SKU" width="110" show-overflow-tooltip /> <el-table-column prop="spec_model" label="规格" width="120" show-overflow-tooltip />
<el-table-column label="序列号/批次" min-width="150" show-overflow-tooltip> <el-table-column prop="stock_qty" label="账面数" width="80" align="center" />
<el-table-column prop="quantity" label="实盘数" width="80" align="center" />
<el-table-column label="差异" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ row.serial_number || row.batch_number || row.batch_no || '-' }}</span> <span :style="{ color: row.diff_qty > 0 ? '#67C23A' : row.diff_qty < 0 ? '#F56C6C' : '#909399' }">
</template> {{ row.diff_qty > 0 ? '+' : '' }}{{ row.diff_qty || 0 }}
</el-table-column> </span>
<el-table-column label="实盘" width="60" align="center">
<template #default="scope">
<span v-if="scope.row.scanned" class="actual-qty-text">{{ scope.row.qty_actual }}</span>
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="scope">
<el-button
v-if="userStore.hasPermission('inventory_stocktake:operation')"
type="primary"
link
icon="Edit"
@click.stop="openQtyDialog(scope.row)"
>
修改
</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
</el-table> </el-table>
</div> </div>
<!-- 分页 -->
<div class="drawer-footer" style="display: flex; justify-content: flex-end; padding: 10px;">
<el-pagination
v-model:current-page="listPage"
v-model:page-size="listLimit"
:page-sizes="[20, 50, 100, 200]"
:total="listTotal"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleListLimitChange"
@current-change="handleListPageChange"
/>
</div>
<div class="drawer-footer"> <div class="drawer-footer">
<el-button @click="showList = false" style="width: 100%">关闭列表</el-button> <el-button @click="showList = false" style="width: 100%">关闭列表</el-button>
</div> </div>
@ -443,6 +436,14 @@ const showVarianceDialog = ref(false)
// ★ 新增: 差异列表搜索 // ★ 新增: 差异列表搜索
const searchSku = ref('') const searchSku = ref('')
// ★ 新增: 盘点清单弹窗分页
const listPage = ref(1)
const listLimit = ref(20)
const listTotal = ref(0)
const listKeyword = ref('')
const listLoading = ref(false)
const listData = ref<any[]>([])
// ★ 新增: 盘点开始防呆倒计时 // ★ 新增: 盘点开始防呆倒计时
const countdown = ref(0) const countdown = ref(0)
let countdownTimer: any = null let countdownTimer: any = null
@ -606,9 +607,10 @@ const checkServerDraft = async () => {
const res: any = await request({ const res: any = await request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', method: 'get',
params: {} params: { page: 1, limit: 1 } // 只获取一条记录来获取总数
}) })
serverDraftCount.value = (res && res.length) || 0 // 后端返回格式已改为 { items: [], total: number }
serverDraftCount.value = (res && res.total) || 0
} catch (e) {} } catch (e) {}
} }
@ -670,12 +672,15 @@ const resumeSession = async () => {
btnLoading.value = true btnLoading.value = true
try { try {
// 获取最新的会话(后端已移除 is_finished 字段) // 获取最新的会话(后端已移除 is_finished 字段)
const drafts: any = await request({ // 后端返回格式已改为 { items: [], total: number }
const res: any = await request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
method: 'get', method: 'get',
params: {} params: { page: 1, limit: 10000 } // 获取足够多的数据
}) })
const drafts = res && res.items ? res.items : []
if (!drafts || drafts.length === 0) { if (!drafts || drafts.length === 0) {
ElMessage.warning('没有找到未完成的盘点记录') ElMessage.warning('没有找到未完成的盘点记录')
return return
@ -981,7 +986,55 @@ const filteredVarianceList = computed(() => {
) )
}) })
const openInventoryList = () => { showList.value = true } // ★ 新增: 获取盘点清单数据(后端分页)
const fetchInventoryList = async () => {
listLoading.value = true
try {
const res: any = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: {
page: listPage.value,
limit: listLimit.value,
keyword: listKeyword.value
}
})
if (res) {
listData.value = res.items || []
listTotal.value = res.total || 0
}
} catch (e) {
ElMessage.error('获取盘点清单失败')
} finally {
listLoading.value = false
}
}
// ★ 修改: 打开盘点清单弹窗
const openInventoryList = async () => {
showList.value = true
listPage.value = 1
listKeyword.value = ''
await fetchInventoryList()
}
// ★ 新增: 盘点清单搜索
const handleListSearch = () => {
listPage.value = 1
fetchInventoryList()
}
// ★ 新增: 盘点清单分页变化
const handleListPageChange = (page: number) => {
listPage.value = page
fetchInventoryList()
}
const handleListLimitChange = (limit: number) => {
listLimit.value = limit
listPage.value = 1
fetchInventoryList()
}
// ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗 // ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗
const openFinishDialog = () => { const openFinishDialog = () => {