From c361d25ea0831d42ea9fa7d95fbf09bd3a658a29 Mon Sep 17 00:00:00 2001 From: DXC Date: Thu, 2 Apr 2026 19:06:19 +0800 Subject: [PATCH] fix(stocktake): display batch number in scanning dialog and fix empty uncounted items in Excel export --- inventory-backend/app/api/v1/inbound/stock.py | 55 ++++++++++++------- .../src/views/stock/stocktake/index.vue | 4 +- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index e538836..e2642e3 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -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), diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index 1eee0c4..14a9cfc 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -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: {