fix: correct excel export formatting (timezone, spec, user, location) and add auto-polling for collaborative stocktake
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
from flask import Blueprint, jsonify, request, send_file
|
from flask import Blueprint, jsonify, request, send_file
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from app.utils.decorators import permission_required
|
from app.utils.decorators import permission_required
|
||||||
import uuid as uuid_module
|
import uuid as uuid_module
|
||||||
import io
|
import io
|
||||||
@ -14,6 +14,12 @@ from app.models.inbound.stocktake import StocktakeDraft
|
|||||||
from app.models.transaction import TransBorrow
|
from app.models.transaction import TransBorrow
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
|
||||||
|
# 尝试导入用户模型
|
||||||
|
try:
|
||||||
|
from app.models.sys.user import User
|
||||||
|
except ImportError:
|
||||||
|
User = None
|
||||||
|
|
||||||
# 尝试导入半成品和成品
|
# 尝试导入半成品和成品
|
||||||
try:
|
try:
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
@ -553,10 +559,10 @@ def export_stocktake():
|
|||||||
elif source_table == 'stock_product':
|
elif source_table == 'stock_product':
|
||||||
stock = StockProduct.query.get(stock_id) if StockProduct else None
|
stock = StockProduct.query.get(stock_id) if StockProduct else None
|
||||||
else:
|
else:
|
||||||
return {'name': '-', 'sku': '-', 'spec': '-', 'unit': '-'}
|
return {'name': '-', 'sku': '-', 'spec': '-', 'unit': '-', 'location': '-'}
|
||||||
|
|
||||||
if not stock:
|
if not stock:
|
||||||
return {'name': '-', 'sku': '-', 'spec': '-', 'unit': '-'}
|
return {'name': '-', 'sku': '-', 'spec': '-', 'unit': '-', 'location': '-'}
|
||||||
|
|
||||||
# 安全获取 sku
|
# 安全获取 sku
|
||||||
stock_sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', None) or '-'
|
stock_sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', None) or '-'
|
||||||
@ -571,13 +577,56 @@ def export_stocktake():
|
|||||||
if hasattr(MaterialBase, 'code') and stock_sku != '-':
|
if hasattr(MaterialBase, 'code') and stock_sku != '-':
|
||||||
material = MaterialBase.query.filter_by(code=stock_sku).first()
|
material = MaterialBase.query.filter_by(code=stock_sku).first()
|
||||||
|
|
||||||
|
# 规格型号:优先从 material 取,再 fallback 到 stock
|
||||||
|
spec = (
|
||||||
|
getattr(material, 'specification', None) or
|
||||||
|
getattr(material, 'spec', None) or
|
||||||
|
getattr(stock, 'spec_model', None) or
|
||||||
|
getattr(stock, 'standard', None) or
|
||||||
|
'-'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 库位:获取真实物理库位
|
||||||
|
location = getattr(stock, 'location', None) or getattr(stock, 'position', None) or '-'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'name': material.name if material else stock_sku,
|
'name': material.name if material else stock_sku,
|
||||||
'sku': stock_sku,
|
'sku': stock_sku,
|
||||||
'spec': getattr(stock, 'spec_model', None) or getattr(stock, 'standard', None) or '-',
|
'spec': spec,
|
||||||
'unit': getattr(stock, 'unit', None) or '个'
|
'unit': getattr(stock, 'unit', None) or '个',
|
||||||
|
'location': location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_user_name(user_id):
|
||||||
|
"""获取用户真实姓名或昵称"""
|
||||||
|
if not User or not user_id:
|
||||||
|
return str(user_id) if user_id else '-'
|
||||||
|
try:
|
||||||
|
user = None
|
||||||
|
# 尝试通过ID或用户名查找
|
||||||
|
if str(user_id).isdigit():
|
||||||
|
user = User.query.get(int(user_id))
|
||||||
|
if not user:
|
||||||
|
user = User.query.filter_by(username=str(user_id)).first()
|
||||||
|
if not user:
|
||||||
|
user = User.query.filter_by(username=str(user_id).upper()).first()
|
||||||
|
if not user:
|
||||||
|
user = User.query.filter_by(username=str(user_id).lower()).first()
|
||||||
|
return getattr(user, 'real_name', None) or getattr(user, 'nickname', None) or str(user_id)
|
||||||
|
except:
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
def to_beijing_time(dt):
|
||||||
|
"""转换为北京时间(+8小时)"""
|
||||||
|
if not dt:
|
||||||
|
return ''
|
||||||
|
try:
|
||||||
|
if isinstance(dt, str):
|
||||||
|
return dt[:19]
|
||||||
|
return (dt + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
except:
|
||||||
|
return str(dt)[:19]
|
||||||
|
|
||||||
def set_header_row(ws, headers):
|
def set_header_row(ws, headers):
|
||||||
for col, header in enumerate(headers, 1):
|
for col, header in enumerate(headers, 1):
|
||||||
cell = ws.cell(row=1, column=col, value=header)
|
cell = ws.cell(row=1, column=col, value=header)
|
||||||
@ -597,12 +646,12 @@ def export_stocktake():
|
|||||||
ws1.cell(row=row_idx, column=1, value=mat_info['name']).border = thin_border
|
ws1.cell(row=row_idx, column=1, value=mat_info['name']).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=2, value=mat_info['sku']).border = thin_border
|
ws1.cell(row=row_idx, column=2, value=mat_info['sku']).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=3, value=mat_info['spec']).border = thin_border
|
ws1.cell(row=row_idx, column=3, value=mat_info['spec']).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=4, value=draft.source_table).border = thin_border
|
ws1.cell(row=row_idx, column=4, value=mat_info['location']).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=5, value=float(draft.stock_qty or 0)).border = thin_border
|
ws1.cell(row=row_idx, column=5, value=float(draft.stock_qty or 0)).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=6, value=float(draft.quantity or 0)).border = thin_border
|
ws1.cell(row=row_idx, column=6, value=float(draft.quantity or 0)).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=7, value=float(draft.diff_qty or 0)).border = thin_border
|
ws1.cell(row=row_idx, column=7, value=float(draft.diff_qty or 0)).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=8, value=draft.user_id or '').border = thin_border
|
ws1.cell(row=row_idx, column=8, value=get_user_name(draft.user_id)).border = thin_border
|
||||||
ws1.cell(row=row_idx, column=9, value=str(draft.scan_time)[:19] if draft.scan_time else '').border = thin_border
|
ws1.cell(row=row_idx, column=9, value=to_beijing_time(draft.scan_time)).border = thin_border
|
||||||
|
|
||||||
# ===== Sheet 2: 账实相符明细 =====
|
# ===== Sheet 2: 账实相符明细 =====
|
||||||
ws2 = wb.create_sheet("账实相符明细")
|
ws2 = wb.create_sheet("账实相符明细")
|
||||||
@ -615,12 +664,12 @@ def export_stocktake():
|
|||||||
ws2.cell(row=row_idx, column=1, value=mat_info['name']).border = thin_border
|
ws2.cell(row=row_idx, column=1, value=mat_info['name']).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=2, value=mat_info['sku']).border = thin_border
|
ws2.cell(row=row_idx, column=2, value=mat_info['sku']).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=3, value=mat_info['spec']).border = thin_border
|
ws2.cell(row=row_idx, column=3, value=mat_info['spec']).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=4, value=draft.source_table).border = thin_border
|
ws2.cell(row=row_idx, column=4, value=mat_info['location']).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=5, value=float(draft.stock_qty or 0)).border = thin_border
|
ws2.cell(row=row_idx, column=5, value=float(draft.stock_qty or 0)).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=6, value=float(draft.quantity or 0)).border = thin_border
|
ws2.cell(row=row_idx, column=6, value=float(draft.quantity or 0)).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=7, value=float(draft.diff_qty or 0)).border = thin_border
|
ws2.cell(row=row_idx, column=7, value=float(draft.diff_qty or 0)).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=8, value=draft.user_id or '').border = thin_border
|
ws2.cell(row=row_idx, column=8, value=get_user_name(draft.user_id)).border = thin_border
|
||||||
ws2.cell(row=row_idx, column=9, value=str(draft.scan_time)[:19] if draft.scan_time else '').border = thin_border
|
ws2.cell(row=row_idx, column=9, value=to_beijing_time(draft.scan_time)).border = thin_border
|
||||||
|
|
||||||
# ===== Sheet 3: 外借在用资产明细 =====
|
# ===== Sheet 3: 外借在用资产明细 =====
|
||||||
ws3 = wb.create_sheet("外借在用资产明细")
|
ws3 = wb.create_sheet("外借在用资产明细")
|
||||||
@ -643,8 +692,8 @@ def export_stocktake():
|
|||||||
ws3.cell(row=row_idx, column=6, value=total_qty).border = thin_border
|
ws3.cell(row=row_idx, column=6, value=total_qty).border = thin_border
|
||||||
ws3.cell(row=row_idx, column=7, value=returned_qty).border = thin_border
|
ws3.cell(row=row_idx, column=7, value=returned_qty).border = thin_border
|
||||||
ws3.cell(row=row_idx, column=8, value=pending_qty).border = thin_border
|
ws3.cell(row=row_idx, column=8, value=pending_qty).border = thin_border
|
||||||
ws3.cell(row=row_idx, column=9, value=str(borrow.borrow_time)[:19] if borrow.borrow_time else '').border = thin_border
|
ws3.cell(row=row_idx, column=9, value=to_beijing_time(borrow.borrow_time)).border = thin_border
|
||||||
ws3.cell(row=row_idx, column=10, value='无限期' if not borrow.expected_return_time else str(borrow.expected_return_time)[:10]).border = thin_border
|
ws3.cell(row=row_idx, column=10, value='无限期' if not borrow.expected_return_time else to_beijing_time(borrow.expected_return_time)).border = thin_border
|
||||||
|
|
||||||
# 调整列宽
|
# 调整列宽
|
||||||
for ws in [ws1, ws2, ws3]:
|
for ws in [ws1, ws2, ws3]:
|
||||||
|
|||||||
@ -349,7 +349,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { getAllStock } from '@/api/inbound/stock'
|
import { getAllStock } from '@/api/inbound/stock'
|
||||||
import QrScanner from '@/components/QrScanner/index.vue'
|
import QrScanner from '@/components/QrScanner/index.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
@ -418,6 +418,51 @@ const currentItem = ref<StockItem | null>(null)
|
|||||||
const inputQty = ref<number | undefined>(undefined)
|
const inputQty = ref<number | undefined>(undefined)
|
||||||
const qtyInputRef = ref()
|
const qtyInputRef = ref()
|
||||||
|
|
||||||
|
// ★ 新增: 多人协同心跳刷新定时器
|
||||||
|
let syncTimer: any = null
|
||||||
|
|
||||||
|
// ★ 新增: 静默刷新数据(不弹loading)
|
||||||
|
const syncData = async () => {
|
||||||
|
try {
|
||||||
|
// 仅刷新差异列表数据,不显示loading
|
||||||
|
const res = await getAllStock()
|
||||||
|
if (!res) return
|
||||||
|
|
||||||
|
const processItem = (item: any, type: string) => {
|
||||||
|
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
|
||||||
|
if (stock <= 0) return
|
||||||
|
const name = item.material_name || item.product_name || item.name || '未知物品'
|
||||||
|
const uuid = item.uuid || item.sku || ''
|
||||||
|
const isScanned = scannedMap.value.has(uuid)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
name: name,
|
||||||
|
standard: item.spec_model || item.standard || item.model || '',
|
||||||
|
sku: item.sku || '',
|
||||||
|
uuid: uuid,
|
||||||
|
bar_code: item.bar_code || item.barcode || '',
|
||||||
|
qty_stock: stock,
|
||||||
|
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
|
||||||
|
scanned: isScanned,
|
||||||
|
uniqueKey: `${type}_${item.id}`,
|
||||||
|
source_table: typeToSourceTable(type),
|
||||||
|
stock_id: item.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list: StockItem[] = []
|
||||||
|
if (res.materials) res.materials.forEach((i: any) => { const item = processItem(i, 'material'); if (item) list.push(item) })
|
||||||
|
if (res.semis) res.semis.forEach((i: any) => { const item = processItem(i, 'semi'); if (item) list.push(item) })
|
||||||
|
if (res.products) res.products.forEach((i: any) => { const item = processItem(i, 'product'); if (item) list.push(item) })
|
||||||
|
|
||||||
|
// 静默更新数据(不触发loading)
|
||||||
|
allData.value = list
|
||||||
|
await fetchBorrowedQuantities(list)
|
||||||
|
} catch (e) {
|
||||||
|
// 静默失败,不弹错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
getDrafts: (sessionId?: string) => request({
|
getDrafts: (sessionId?: string) => request({
|
||||||
url: '/v1/inbound/stock/draft/list',
|
url: '/v1/inbound/stock/draft/list',
|
||||||
@ -498,6 +543,18 @@ async function fetchBorrowedQuantities(items: StockItem[]): Promise<void> {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkServerDraft()
|
await checkServerDraft()
|
||||||
|
// ★ 启动多人协同心跳轮询(每5秒静默刷新)
|
||||||
|
syncTimer = setInterval(() => {
|
||||||
|
syncData()
|
||||||
|
}, 5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ★ 新增: 组件卸载时清除定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (syncTimer) {
|
||||||
|
clearInterval(syncTimer)
|
||||||
|
syncTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkServerDraft = async () => {
|
const checkServerDraft = async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user