From f3b60dfc541b06236578ef27302080449a872b3b Mon Sep 17 00:00:00 2001 From: dxc Date: Thu, 5 Feb 2026 10:20:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=BA=E5=BA=93=E6=93=8D=E4=BD=9C=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=8A=E9=9D=A2=E5=AE=9E=E7=8E=B0=EF=BC=8C=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E8=B7=91=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inventory-backend/app/api/v1/outbound.py | 45 +++-- inventory-backend/app/models/outbound.py | 6 +- .../app/services/outbound_service.py | 123 +++++++++----- inventory-web/src/api/common/upload.ts | 3 +- inventory-web/src/api/outbound.ts | 10 +- inventory-web/src/views/outbound/create.vue | 155 ++++++++++++++---- inventory-web/src/views/outbound/index.vue | 110 ++++++++++--- inventory-web/vite.config.ts | 2 +- 8 files changed, 337 insertions(+), 117 deletions(-) diff --git a/inventory-backend/app/api/v1/outbound.py b/inventory-backend/app/api/v1/outbound.py index 079c150..1899ddf 100644 --- a/inventory-backend/app/api/v1/outbound.py +++ b/inventory-backend/app/api/v1/outbound.py @@ -1,12 +1,13 @@ from flask import Blueprint, request, jsonify from app.services.outbound_service import OutboundService from flask_jwt_extended import jwt_required, get_jwt_identity +import traceback outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound') # -------------------------------------------------------- -# 1. 扫码查询库存接口 +# 1. 扫码查询库存接口 (关联三个库存表) # GET /api/v1/outbound/scan?barcode=... # -------------------------------------------------------- @outbound_bp.route('/scan', methods=['GET']) @@ -17,13 +18,24 @@ def scan_barcode(): return jsonify({'code': 400, 'msg': '请提供条码'}), 400 try: + # 调用 Service 层去三个表中查找 result = OutboundService.get_stock_by_barcode(barcode) + if result: - return jsonify({'code': 200, 'data': result, 'msg': '扫描成功'}) + return jsonify({ + 'code': 200, + 'msg': '扫描成功', + 'data': result + }) else: - return jsonify({'code': 404, 'msg': '未找到对应的库存记录'}), 404 + return jsonify({ + 'code': 404, + 'msg': '未找到对应的库存记录,请确认条码是否正确' + }), 404 + except Exception as e: - return jsonify({'code': 500, 'msg': str(e)}), 500 + traceback.print_exc() + return jsonify({'code': 500, 'msg': f'扫描查询出错: {str(e)}'}), 500 # -------------------------------------------------------- @@ -37,24 +49,37 @@ def create_outbound(): if not data: return jsonify({'code': 400, 'msg': '无有效数据'}), 400 - current_user = get_jwt_identity() # 获取当前登录用户作为操作员 + # 获取当前登录用户名 (JWT identity) + current_user_name = get_jwt_identity() + if not current_user_name: + current_user_name = 'Unknown' - # 简单的必填校验 (更复杂的校验可放入 Schema) + # ★ [修改] 获取最终的操作员名称 + # 优先取前端传来的 operator_name (你在前端下拉框选的人) + # 如果前端没传,则回退使用当前登录用户的名字 + final_operator = data.get('operator_name') + if not final_operator: + final_operator = current_user_name + + # 必填校验 required_fields = ['stock_id', 'source_table', 'quantity', 'consumer_name', 'signature_path'] for field in required_fields: if field not in data or not data[field]: return jsonify({'code': 400, 'msg': f'缺少必填字段: {field}'}), 400 try: - outbound_record = OutboundService.create_outbound(data, operator_name=current_user) + # ★ [修改] 将确认后的操作员名称传给 Service + outbound_record = OutboundService.create_outbound(data, operator_name=final_operator) return jsonify({ 'code': 200, 'msg': '出库成功', 'data': outbound_record.to_dict() }) except ValueError as e: + # 业务逻辑错误 (如库存不足) return jsonify({'code': 400, 'msg': str(e)}), 400 except Exception as e: + traceback.print_exc() return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500 @@ -67,11 +92,10 @@ def create_outbound(): def get_outbound_list(): try: page = int(request.args.get('page', 1)) - per_page = int(request.args.get('limit', 10)) + limit = int(request.args.get('limit', 10)) keyword = request.args.get('keyword', '') - # 日期范围处理可根据前端传参格式调整 - result = OutboundService.get_list(page, per_page, keyword) + result = OutboundService.get_list(page, limit, keyword) return jsonify({ 'code': 200, @@ -79,4 +103,5 @@ def get_outbound_list(): 'data': result }) except Exception as e: + traceback.print_exc() return jsonify({'code': 500, 'msg': str(e)}), 500 \ No newline at end of file diff --git a/inventory-backend/app/models/outbound.py b/inventory-backend/app/models/outbound.py index e1b3cef..62e17fa 100644 --- a/inventory-backend/app/models/outbound.py +++ b/inventory-backend/app/models/outbound.py @@ -10,17 +10,17 @@ class TransOutbound(db.Model): # 关联源库存信息 sku = db.Column(db.String(100)) - source_table = db.Column(db.String(50)) # 'stock_buy', 'stock_product', etc. + source_table = db.Column(db.String(50)) # 'stock_buy', 'stock_product', 'stock_semi' stock_id = db.Column(db.Integer) # 对应源表的主键ID barcode = db.Column(db.String(100)) # 实际扫码内容 # 业务信息 - outbound_type = db.Column(db.String(50), default='SALES') # SALES, USE, TRANSFER + outbound_type = db.Column(db.String(50), default='SALES') # SALES(销售), USE(领用), TRANSFER(调拨) quantity = db.Column(db.Numeric(19, 4), nullable=False) # 签字与追溯 consumer_name = db.Column(db.String(100)) # 领用人/客户 - signature_path = db.Column(db.Text) # 签名图片路径 + signature_path = db.Column(db.Text) # 电子签名图片路径 outbound_time = db.Column(db.DateTime, default=datetime.now) operator_name = db.Column(db.String(100)) # 操作员 diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index 44d1a92..a511f31 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -4,10 +4,12 @@ from sqlalchemy import or_ from app.extensions import db from app.models.outbound import TransOutbound -# 导入所有库存实体模型,用于查找和扣减 +# 引入所有库存模型以进行查询 from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from app.models.inbound.product import StockProduct +# ★★★ [关键] 引入基础信息表,用于手动补全名称 +from app.models.base import MaterialBase class OutboundService: @@ -22,58 +24,92 @@ class OutboundService: @staticmethod def get_stock_by_barcode(barcode): """ - 根据条码在各个库存表中查找 - 优先级: 成品 -> 半成品 -> 采购件 + [核心逻辑] 根据扫码内容查找对应的库存物品 + 查找顺序: 成品 (StockProduct) -> 半成品 (StockSemi) -> 采购件 (StockBuy) + 匹配逻辑: 匹配 barcode 字段 OR sku 字段 """ if not barcode: return None - # 1. 查成品 - prod = StockProduct.query.filter_by(barcode=barcode).first() + clean_code = barcode.strip() + + # --- 1. 查找成品表 (StockProduct) --- + prod = StockProduct.query.filter( + or_(StockProduct.barcode == clean_code, StockProduct.sku == clean_code) + ).first() if prod: - return OutboundService._format_scan_result(prod, 'stock_product', prod.sku) + return OutboundService._format_scan_result(prod, 'stock_product') - # 2. 查半成品 - semi = StockSemi.query.filter_by(barcode=barcode).first() + # --- 2. 查找半成品表 (StockSemi) --- + semi = StockSemi.query.filter( + or_(StockSemi.barcode == clean_code, StockSemi.sku == clean_code) + ).first() if semi: - return OutboundService._format_scan_result(semi, 'stock_semi', semi.sku) + return OutboundService._format_scan_result(semi, 'stock_semi') - # 3. 查采购件 - buy = StockBuy.query.filter_by(barcode=barcode).first() + # --- 3. 查找采购件表 (StockBuy) --- + buy = StockBuy.query.filter( + or_(StockBuy.barcode == clean_code, StockBuy.sku == clean_code) + ).first() if buy: - # 采购件可能需要关联 material_base 获取名称,这里假设 base_id 关联已建立 - name = buy.base.name if buy.base else "未知采购件" - spec = buy.base.spec_model if buy.base else "" - return OutboundService._format_scan_result(buy, 'stock_buy', buy.sku, name, spec) + return OutboundService._format_scan_result(buy, 'stock_buy') return None @staticmethod - def _format_scan_result(item, table_name, sku, name=None, spec=None): - """格式化返回给前端的数据结构""" - # 如果没有传 name (例如成品/半成品),尝试通过关联获取,或者直接用 SKU 代替 - item_name = name - item_spec = spec + def _format_scan_result(item, table_name): + """ + [核心修复] 格式化返回数据,确保名称和规格一定能取到 + """ + base_name = "" + base_spec = "" - if not item_name and hasattr(item, 'base') and item.base: - item_name = item.base.name - item_spec = item.base.spec_model + # ------------------------------------------------------- + # 修复逻辑:强制获取基础信息 + # ------------------------------------------------------- + + # 步骤 1: 尝试通过 ORM 关联获取 (如果有定义 relationship) + if hasattr(item, 'base') and item.base: + base_name = item.base.name + base_spec = item.base.spec_model + + # 步骤 2: [关键] 如果步骤1失败,但有 base_id,则手动查询 MaterialBase 表 + # 这能解决“扫码有库存但显示未知物品”的问题 + if not base_name and hasattr(item, 'base_id') and item.base_id: + try: + # 手动查基础表 + base_info = MaterialBase.query.get(item.base_id) + if base_info: + base_name = base_info.name + base_spec = base_info.spec_model + except Exception as e: + print(f"基础信息查询失败: {e}") + + # 步骤 3: 兜底逻辑,某些旧表可能直接存了 material_name 字段 + if not base_name and hasattr(item, 'material_name'): + base_name = item.material_name + + stock_qty = float(item.stock_quantity) if item.stock_quantity else 0 + avail_qty = float(item.available_quantity) if item.available_quantity else 0 return { 'id': item.id, - 'sku': sku, - 'name': item_name or sku, - 'spec_model': item_spec or '', - 'source_table': table_name, - 'stock_quantity': float(item.stock_quantity), - 'available_quantity': float(item.available_quantity), + 'sku': item.sku, + 'name': base_name or "未知物品", # 此时应该能正确显示名称了 + 'spec_model': base_spec or "", # 此时应该能正确显示规格了 + 'source_table': table_name, # 标记来源表 + 'stock_quantity': stock_qty, # 当前库存 + 'available_quantity': avail_qty, # 可用库存 'batch_number': getattr(item, 'batch_number', ''), - 'warehouse_location': getattr(item, 'warehouse_location', '') + 'warehouse_location': getattr(item, 'warehouse_location', ''), + 'barcode': getattr(item, 'barcode', '') } @staticmethod def create_outbound(data, operator_name='System'): - """执行出库逻辑:扣减库存 + 记录日志""" + """ + [核心逻辑] 执行出库:扣减对应表的库存 + 创建记录 + """ source_table = data.get('source_table') stock_id = data.get('stock_id') quantity = float(data.get('quantity', 0)) @@ -81,7 +117,7 @@ class OutboundService: if quantity <= 0: raise ValueError("出库数量必须大于0") - # 1. 获取对应的库存记录模型 + # 1. 动态映射表模型 model_map = { 'stock_buy': StockBuy, 'stock_semi': StockSemi, @@ -90,22 +126,24 @@ class OutboundService: ModelClass = model_map.get(source_table) if not ModelClass: - raise ValueError(f"未知的库存来源表: {source_table}") + raise ValueError(f"无效的数据来源表: {source_table}") - # 2. 锁定并查询库存 (使用 with_for_update 防止并发扣减) + # 2. 锁定并查询库存 (使用 with_for_update 防止并发超卖) stock_item = ModelClass.query.with_for_update().get(stock_id) if not stock_item: - raise ValueError("库存记录不存在") + raise ValueError("库存记录不存在或已被删除") - if stock_item.available_quantity < quantity: - raise ValueError(f"库存不足!当前可用: {stock_item.available_quantity}, 请求出库: {quantity}") + # 3. 校验库存充足 + current_avail = float(stock_item.available_quantity) + if current_avail < quantity: + raise ValueError(f"库存不足!当前可用: {current_avail}, 请求出库: {quantity}") try: - # 3. 扣减库存 - stock_item.stock_quantity -= quantity - stock_item.available_quantity -= quantity + # 4. 扣减库存 + stock_item.stock_quantity = float(stock_item.stock_quantity) - quantity + stock_item.available_quantity = float(stock_item.available_quantity) - quantity - # 4. 创建出库记录 + # 5. 创建出库记录 new_outbound = TransOutbound( outbound_no=OutboundService.generate_outbound_no(), sku=data.get('sku'), @@ -131,6 +169,7 @@ class OutboundService: @staticmethod def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None): + """查询出库历史记录""" query = TransOutbound.query.order_by(TransOutbound.outbound_time.desc()) if keyword: @@ -141,10 +180,10 @@ class OutboundService: )) if start_date and end_date: - # 假设传入的是 'YYYY-MM-DD',需要处理时间范围 query = query.filter(TransOutbound.outbound_time.between(start_date, end_date)) pagination = query.paginate(page=page, per_page=per_page, error_out=False) + return { 'items': [item.to_dict() for item in pagination.items], 'total': pagination.total, diff --git a/inventory-web/src/api/common/upload.ts b/inventory-web/src/api/common/upload.ts index def1399..f24d158 100644 --- a/inventory-web/src/api/common/upload.ts +++ b/inventory-web/src/api/common/upload.ts @@ -9,7 +9,8 @@ export function uploadFile(file: File) { formData.append('file', file) return request({ - url: '/api/v1/common/upload', + // ★★★ [修改] 去掉开头的 /api,适配 request.ts 的 baseURL + url: '/v1/common/upload', method: 'post', data: formData, headers: { diff --git a/inventory-web/src/api/outbound.ts b/inventory-web/src/api/outbound.ts index efb3271..574ba35 100644 --- a/inventory-web/src/api/outbound.ts +++ b/inventory-web/src/api/outbound.ts @@ -22,6 +22,7 @@ export interface ScanResult { available_quantity: number batch_number?: string warehouse_location?: string + barcode?: string } /** @@ -30,7 +31,8 @@ export interface ScanResult { */ export function getStockByBarcode(barcode: string) { return request({ - url: '/api/v1/outbound/scan', + // ★★★ [修改] 去掉开头的 /api,Axios 会自动拼接 baseURL + url: '/v1/outbound/scan', method: 'get', params: { barcode } }) @@ -41,7 +43,8 @@ export function getStockByBarcode(barcode: string) { */ export function submitOutbound(data: OutboundSubmitData) { return request({ - url: '/api/v1/outbound', + // ★★★ [修改] 去掉开头的 /api + url: '/v1/outbound', method: 'post', data }) @@ -52,7 +55,8 @@ export function submitOutbound(data: OutboundSubmitData) { */ export function getOutboundList(params: any) { return request({ - url: '/api/v1/outbound', + // ★★★ [修改] 去掉开头的 /api + url: '/v1/outbound', method: 'get', params }) diff --git a/inventory-web/src/views/outbound/create.vue b/inventory-web/src/views/outbound/create.vue index 71fd2d2..49e811a 100644 --- a/inventory-web/src/views/outbound/create.vue +++ b/inventory-web/src/views/outbound/create.vue @@ -35,7 +35,11 @@ {{ currentItem.name }} {{ currentItem.spec_model }} - {{ currentItem.stock_quantity }} + + + {{ currentItem.stock_quantity }} (可用: {{ currentItem.available_quantity }}) + + {{ currentItem.warehouse_location || '暂无' }} @@ -52,14 +56,37 @@ - + - - - + + + + + + + + + + + + + + @@ -80,13 +107,16 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/inventory-web/vite.config.ts b/inventory-web/vite.config.ts index 76052f0..3bc881e 100644 --- a/inventory-web/vite.config.ts +++ b/inventory-web/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ // 允许局域网访问前端页面 host: '0.0.0.0', port: 5173, - https: true, // ★ [新增] 强制开启 HTTPS,否则浏览器会拦截摄像头 + https: false, // ★ [新增] 强制开启 HTTPS,否则浏览器会拦截摄像头 proxy: { // 拦截所有以 /api 开头的请求 '/api': {