fix(stocktake): display batch number in scanning dialog and fix empty uncounted items in Excel export

This commit is contained in:
DXC
2026-04-02 19:06:19 +08:00
parent dbcb7d0d92
commit c361d25ea0
2 changed files with 38 additions and 21 deletions

View File

@ -716,8 +716,12 @@ def export_stocktake():
1. 盘点差异明细 (diff_qty != 0)
2. 账实相符明细 (diff_qty == 0)
3. 外借在用资产明细 (未归还的借出记录)
4. 未盘点明细(疑似漏盘)
"""
try:
# ★ 获取 session_id 参数,用于过滤当前会话的扫描记录
session_id = request.args.get('session_id', '', type=str)
# 创建工作簿
wb = Workbook()
wb.remove(wb.active)
@ -933,12 +937,17 @@ def export_stocktake():
# ===== Sheet 5: 未盘点明细(疑似漏盘) =====
# 逻辑:获取已盘点的集合,遍历库存表,找出未盘点且有库存的物资
ws5 = wb.create_sheet("未盘点明细(疑似漏盘)")
unscanned_headers = ["物料名称", "SKU", "规格型号", "库位", "调整后账面数", "实盘数", "差异数", "状态"]
unscanned_headers = ["物料名称", "SKU", "规格型号", "库位", "批号", "调整后账面数", "实盘数", "差异数", "状态"]
set_header_row(ws5, unscanned_headers)
# 获取已盘点的 (source_table, stock_id) 集合
all_drafts = StocktakeDraft.query.all()
scanned_set = {(d.source_table, d.stock_id) for d in all_drafts}
# ★ 修复:只查询当前 session_id 的扫描记录,避免历史记录干扰
if session_id:
session_drafts = StocktakeDraft.query.filter_by(session_id=session_id).all()
else:
# 如果没有传 session_id使用所有记录兼容旧行为
session_drafts = StocktakeDraft.query.all()
scanned_set = {(d.source_table, d.stock_id) for d in session_drafts}
def get_borrowed_qty(source_table, stock_id):
"""获取某库存的借出未还数量"""
@ -955,16 +964,14 @@ def export_stocktake():
unscanned_items = []
# ★ 修复 N+1 查询:使用 joinedload 预加载 base 关系
for stock in StockBuy.query.options(joinedload(StockBuy.base)).all():
# ★ 修复 N+1 查询:使用 joinedload 预加载 base 关系,同时过滤 stock_quantity > 0
for stock in StockBuy.query.filter(StockBuy.stock_quantity > 0).options(joinedload(StockBuy.base)).all():
key = ('stock_buy', stock.id)
if key in scanned_set:
continue
stock_qty = float(stock.stock_quantity or 0)
if stock_qty <= 0:
continue
# 扣除外借数量
borrowed_qty = get_borrowed_qty('stock_buy', stock.id)
stock_qty = float(stock.stock_quantity or 0)
expected_qty = stock_qty - borrowed_qty
if expected_qty > 0:
# ★ 直接使用预加载的 base 关系,避免额外查询
@ -973,13 +980,15 @@ def export_stocktake():
'name': material.name if material else '-',
'sku': getattr(stock, 'sku', None) or '-',
'spec': getattr(material, 'spec_model', None) if material else '-',
'location': getattr(stock, 'warehouse_location', None) or '-'
'location': getattr(stock, 'warehouse_location', None) or '-',
'batch_no': getattr(stock, 'batch_number', None) or '-' # ★ 批号字段
}
unscanned_items.append({
'name': mat_info['name'],
'sku': mat_info['sku'],
'spec': mat_info['spec'],
'location': mat_info['location'],
'batch_no': mat_info['batch_no'], # ★ 批号字段
'stock_qty': expected_qty,
'actual_qty': 0,
'diff_qty': -expected_qty,
@ -988,14 +997,12 @@ def export_stocktake():
# 遍历 StockSemi
if StockSemi:
for stock in StockSemi.query.options(joinedload(StockSemi.base)).all():
for stock in StockSemi.query.filter(StockSemi.stock_quantity > 0).options(joinedload(StockSemi.base)).all():
key = ('stock_semi', stock.id)
if key in scanned_set:
continue
stock_qty = float(stock.stock_quantity or 0)
if stock_qty <= 0:
continue
borrowed_qty = get_borrowed_qty('stock_semi', stock.id)
stock_qty = float(stock.stock_quantity or 0)
expected_qty = stock_qty - borrowed_qty
if expected_qty > 0:
# ★ 直接使用预加载的 base 关系,避免额外查询
@ -1004,13 +1011,15 @@ def export_stocktake():
'name': material.name if material else '-',
'sku': getattr(stock, 'sku', None) or '-',
'spec': getattr(material, 'spec_model', None) if material else '-',
'location': getattr(stock, 'warehouse_location', None) or '-'
'location': getattr(stock, 'warehouse_location', None) or '-',
'batch_no': getattr(stock, 'batch_number', None) or '-' # ★ 批号字段
}
unscanned_items.append({
'name': mat_info['name'],
'sku': mat_info['sku'],
'spec': mat_info['spec'],
'location': mat_info['location'],
'batch_no': mat_info['batch_no'], # ★ 批号字段
'stock_qty': expected_qty,
'actual_qty': 0,
'diff_qty': -expected_qty,
@ -1019,7 +1028,7 @@ def export_stocktake():
# 遍历 StockProduct
if StockProduct:
for stock in StockProduct.query.options(joinedload(StockProduct.base)).all():
for stock in StockProduct.query.filter(StockProduct.stock_quantity > 0).options(joinedload(StockProduct.base)).all():
key = ('stock_product', stock.id)
if key in scanned_set:
continue
@ -1035,13 +1044,15 @@ def export_stocktake():
'name': material.name if material else '-',
'sku': getattr(stock, 'sku', None) or '-',
'spec': getattr(material, 'spec_model', None) if material else '-',
'location': getattr(stock, 'warehouse_location', None) or '-'
'location': getattr(stock, 'warehouse_location', None) or '-',
'batch_no': '-' # ★ 成品无批号字段
}
unscanned_items.append({
'name': mat_info['name'],
'sku': mat_info['sku'],
'spec': mat_info['spec'],
'location': mat_info['location'],
'batch_no': mat_info['batch_no'], # ★ 成品无批号字段
'stock_qty': expected_qty,
'actual_qty': 0,
'diff_qty': -expected_qty,
@ -1055,10 +1066,11 @@ def export_stocktake():
ws5.cell(row=row_idx, column=2, value=item['sku']).border = thin_border
ws5.cell(row=row_idx, column=3, value=item['spec']).border = thin_border
ws5.cell(row=row_idx, column=4, value=item['location']).border = thin_border
ws5.cell(row=row_idx, column=5, value=float(item['stock_qty'])).border = thin_border
ws5.cell(row=row_idx, column=6, value=float(item['actual_qty'])).border = thin_border
ws5.cell(row=row_idx, column=7, value=float(item['diff_qty'])).border = thin_border
ws5.cell(row=row_idx, column=8, value=item['status']).border = thin_border
ws5.cell(row=row_idx, column=5, value=item.get('batch_no', '-')).border = thin_border # ★ 批号
ws5.cell(row=row_idx, column=6, value=float(item['stock_qty'])).border = thin_border
ws5.cell(row=row_idx, column=7, value=float(item['actual_qty'])).border = thin_border
ws5.cell(row=row_idx, column=8, value=float(item['diff_qty'])).border = thin_border
ws5.cell(row=row_idx, column=9, value=item['status']).border = thin_border
# 同时写入 Sheet 1 (汇总表) - 盘点人和时间留空
ws1.cell(row=master_row_idx, column=1, value=item['name']).border = thin_border
ws1.cell(row=master_row_idx, column=2, value=item['sku']).border = thin_border
@ -1227,6 +1239,7 @@ def get_all_stocktake_items():
'id': item.id,
'sku': item.sku or '',
'barcode': item.barcode or '',
'batch_no': item.batch_number or '', # ★ 批号字段
'material_name': item.base.name if item.base else '',
'spec_model': item.base.spec_model if item.base else '',
'stock_qty': float(item.stock_quantity or 0),
@ -1251,6 +1264,7 @@ def get_all_stocktake_items():
'id': item.id,
'sku': item.sku or '',
'barcode': item.barcode or '',
'batch_no': item.batch_number or '', # ★ 批号字段
'material_name': item.base.name if item.base else '',
'spec_model': item.base.spec_model if item.base else '',
'stock_qty': float(item.stock_quantity or 0),
@ -1275,6 +1289,7 @@ def get_all_stocktake_items():
'id': item.id,
'sku': item.sku or '',
'barcode': item.barcode or '',
'batch_no': item.batch_number or '', # ★ 批号字段 (成品无此字段则为空)
'material_name': item.base.name if item.base else '',
'spec_model': item.base.spec_model if item.base else '',
'stock_qty': float(item.stock_quantity or 0),

View File

@ -917,9 +917,11 @@ const exportToExcel = async () => {
// ===== 调试结束 =====
ElMessage.info('正在生成盘点报告,请稍候...');
// ★ 传递 session_id 参数,用于导出当前会话的未盘点明细
const sessionParam = currentSessionId.value ? `?session_id=${encodeURIComponent(currentSessionId.value)}` : '';
// 使用项目封装的 request 发送请求,确保自动携带 JWT Token
const res: any = await request({
url: '/v1/inbound/stock/export-stocktake',
url: '/v1/inbound/stock/export-stocktake' + sessionParam,
method: 'get',
responseType: 'blob' as any, // 核心:接收二进制文件流
headers: {