diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index a6b0d58..7f326f1 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -1,5 +1,5 @@ from flask import Blueprint, jsonify, request, send_file -from app.extensions import db +from app.extensions import db, beijing_time # ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★ from datetime import datetime, timedelta from app.utils.decorators import permission_required @@ -889,3 +889,95 @@ def export_stocktake(): import traceback traceback.print_exc() return jsonify({"message": f"导出失败: {str(e)}"}), 500 + + +# -------------------------------------------------------- +# 生成漏盘数据 - 将未扫描的库存标记为全额盘亏 +# POST /api/v1/inbound/stocktake/generate-missing +# -------------------------------------------------------- +@bp.route('/stocktake/generate-missing', methods=['POST']) +@permission_required('inventory_stocktake:operation') +def generate_missing_stocktake(): + """ + 生成漏盘数据: + 找出所有真实库存 > 0,但未被盘点扫描到的物料, + 自动生成盘点草稿,标记为盘亏(实盘=0,差异=-库存数) + """ + try: + # 1. 获取所有已有盘点记录的 (source_table, stock_id) 集合 + existing_records = db.session.query( + StocktakeDraft.source_table, + StocktakeDraft.stock_id + ).distinct().all() + + scanned_keys = set() + for src_table, stock_id in existing_records: + if stock_id: + scanned_keys.add((src_table, stock_id)) + + # 2. 获取所有真实库存 > 0 的记录 + all_stock = [] + + # 采购库存 + for item in StockBuy.query.filter(StockBuy.stock_quantity > 0).all(): + all_stock.append({ + 'source_table': 'stock_buy', + 'stock_id': item.id, + 'base_id': item.base_id, + 'stock_qty': float(item.stock_quantity or 0) + }) + + # 半成品库存 + if StockSemi: + for item in StockSemi.query.filter(StockSemi.stock_quantity > 0).all(): + all_stock.append({ + 'source_table': 'stock_semi', + 'stock_id': item.id, + 'base_id': item.base_id, + 'stock_qty': float(item.stock_quantity or 0) + }) + + # 成品库存 + if StockProduct: + for item in StockProduct.query.filter(StockProduct.stock_quantity > 0).all(): + all_stock.append({ + 'source_table': 'stock_product', + 'stock_id': item.id, + 'base_id': item.base_id, + 'stock_qty': float(item.stock_quantity or 0) + }) + + # 3. 找出漏盘记录(库存中有但盘点中没有的) + missing_count = 0 + for stock in all_stock: + key = (stock['source_table'], stock['stock_id']) + if key not in scanned_keys: + # 生成漏盘草稿 + draft = StocktakeDraft( + user_id='system', + uuid=f'MISSING-{stock["source_table"]}-{stock["stock_id"]}', + quantity=0, # 实盘数为0 + scan_time=beijing_time(), + session_id='AUTO_GENERATED', + source_table=stock['source_table'], + stock_id=stock['stock_id'], + stock_qty=stock['stock_qty'], + diff_qty=-stock['stock_qty'], # 差异 = 0 - 库存数 = 负数 + remark='未盘点到,系统自动标记为盘亏' + ) + db.session.add(draft) + missing_count += 1 + + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': f'成功生成 {missing_count} 条漏盘记录', + 'data': {'count': missing_count} + }) + + except Exception as e: + db.session.rollback() + import traceback + traceback.print_exc() + return jsonify({'code': 500, 'msg': f'生成漏盘数据失败: {str(e)}'}), 500 diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index b69d8ff..20f1ad0 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -107,6 +107,11 @@ 结束盘点 + + + 结束盘点 (计算漏盘) + + @@ -993,6 +998,46 @@ const openFinishDialog = () => { finishStocktake() } +// ★ 新增:结束盘点(计算漏盘)- 将未扫描的库存标记为全额盘亏 +const handleGenerateMissing = async () => { + if (stats.value.total === 0) { + ElMessage.warning('暂无盘点数据') + return + } + + try { + await ElMessageBox.confirm( + '确认结束当前盘点吗?系统将自动把所有未扫描到的库存标记为全额盘亏!', + '提示', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ) + + btnLoading.value = true + const res = await request({ + url: '/v1/inbound/stocktake/generate-missing', + method: 'post' + }) + + if (res.code === 200) { + ElMessage.success(`成功生成 ${res.data.count} 条漏盘记录`) + // 刷新差异列表 + await checkServerDraft() + } else { + ElMessage.error(res.msg || '生成漏盘数据失败') + } + } catch (e) { + if (e !== 'cancel') { + ElMessage.error('生成漏盘数据失败') + } + } finally { + btnLoading.value = false + } +} + // ★ 重写: 结束盘点 - 纯前端状态流转,不再调用后端 const finishStocktake = async () => { try {