diff --git a/inventory-backend/app/api/v1/inbound/buy.py b/inventory-backend/app/api/v1/inbound/buy.py index e9020b5..a60e658 100644 --- a/inventory-backend/app/api/v1/inbound/buy.py +++ b/inventory-backend/app/api/v1/inbound/buy.py @@ -5,29 +5,19 @@ import traceback inbound_buy_bp = Blueprint('stock_buy', __name__) - # ------------------------------------------------------------------ # 0. 基础物料搜索 # ------------------------------------------------------------------ @inbound_buy_bp.route('/search-base', methods=['GET']) def search_base(): - """ - 供前端下拉框远程搜索使用 - Query Param: keyword (名称或规格) - """ try: keyword = request.args.get('keyword', '') data = BuyInboundService.search_base_material(keyword) - return jsonify({ - "code": 200, - "msg": "success", - "data": data - }) + return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 - # ------------------------------------------------------------------ # 1. 获取列表 # ------------------------------------------------------------------ @@ -37,8 +27,6 @@ def get_list(): page = request.args.get('page', 1, type=int) limit = request.args.get('pageSize', 15, type=int) keyword = request.args.get('keyword', '') - - # 获取状态列表参数 statuses_str = request.args.get('statuses', '') statuses = statuses_str.split(',') if statuses_str else [] @@ -48,7 +36,6 @@ def get_list(): traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 - # ------------------------------------------------------------------ # 2. 新增入库 # ------------------------------------------------------------------ @@ -58,19 +45,12 @@ def submit(): data = request.get_json() if not data: return jsonify({"code": 400, "msg": "No data"}), 400 - new_stock = BuyInboundService.handle_inbound(data) - - return jsonify({ - "code": 200, - "msg": "入库成功", - "data": new_stock.to_dict() - }) + return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 - # ------------------------------------------------------------------ # 3. 更新入库 # ------------------------------------------------------------------ @@ -83,7 +63,6 @@ def update_buy(id): except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 - # ------------------------------------------------------------------ # 4. 删除 # ------------------------------------------------------------------ @@ -95,26 +74,8 @@ def delete_buy(id): except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 - # ------------------------------------------------------------------ -# 5. 获取关联的出库历史 -# ------------------------------------------------------------------ -@inbound_buy_bp.route('//history', methods=['GET']) -def get_history(id): - try: - history = BuyInboundService.get_outbound_history(id) - return jsonify({ - "code": 200, - "msg": "success", - "data": history - }) - except Exception as e: - traceback.print_exc() - return jsonify({"code": 500, "msg": str(e)}), 500 - - -# ------------------------------------------------------------------ -# 6. 供应商建议 +# 5. 供应商建议 (基于 base_id) # ------------------------------------------------------------------ @inbound_buy_bp.route('/suggestions/suppliers', methods=['GET']) def get_supplier_suggestions(): @@ -124,18 +85,17 @@ def get_supplier_suggestions(): data = BuyInboundService.get_history_suppliers(base_id) return jsonify({"code": 200, "msg": "success", "data": data}) - # ------------------------------------------------------------------ -# 7. 系统用户建议 (采购人) +# 6. 采购人建议 (全局,基于 stock_buy 表) # ------------------------------------------------------------------ @inbound_buy_bp.route('/suggestions/users', methods=['GET']) def get_user_suggestions(): keyword = request.args.get('keyword', '') - data = BuyInboundService.search_system_users(keyword) + data = BuyInboundService.get_history_purchasers(keyword) return jsonify({"code": 200, "msg": "success", "data": data}) # ------------------------------------------------------------------ -# 8. [新增] 链接建议 +# 7. 链接建议 (基于 base_id) # ------------------------------------------------------------------ @inbound_buy_bp.route('/suggestions/links', methods=['GET']) def get_link_suggestions(): diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index 253e3e7..0698d6a 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -1,13 +1,6 @@ from app.extensions import db from app.models.inbound.buy import StockBuy from app.models.base import MaterialBase - -# 尝试导入出库模型,如果不存在则忽略 -try: - from app.models.outbound import TransOutbound -except ImportError: - TransOutbound = None - from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ import traceback @@ -53,15 +46,12 @@ class BuyInboundService: @staticmethod def search_base_material(keyword): try: - # 只查询已启用的物料 query = MaterialBase.query.filter(MaterialBase.is_enabled == True) - if keyword: query = query.filter( or_( MaterialBase.name.ilike(f'%{keyword}%'), - MaterialBase.spec_model.ilike(f'%{keyword}%'), - MaterialBase.pinyin.ilike(f'%{keyword}%') + MaterialBase.spec_model.ilike(f'%{keyword}%') ) ) query = query.order_by(MaterialBase.id.desc()).limit(20) @@ -342,50 +332,61 @@ class BuyInboundService: return {"total": 0, "items": []} # ============================================================ - # 6. 供应商历史查询 (根据 base_id) + # 6. 供应商历史查询 (基于 base_id) # ============================================================ @staticmethod def get_history_suppliers(base_id): - """返回该物料关联的供应商列表(去重)""" + """返回该物料在 stock_buy 表中关联过的供应商列表""" try: + # 去重查询 query = db.session.query(StockBuy.supplier_name).filter( StockBuy.base_id == base_id, StockBuy.supplier_name.isnot(None), StockBuy.supplier_name != '' ).distinct().order_by(StockBuy.supplier_name) + suppliers = [row[0] for row in query.all()] return suppliers except Exception: return [] # ============================================================ - # 7. 系统用户搜索 (全局) + # 7. 采购人/邮箱历史查询 (全局,从 stock_buy 获取) # ============================================================ @staticmethod - def search_system_users(keyword): - """搜索系统用户(活跃状态)""" - from app.models.system import SysUser + def get_history_purchasers(keyword): + """ + 从 stock_buy 表中提取历史采购人和邮箱。 + 不绑定 base_id,因为采购人通常是全局的。 + """ try: - query = SysUser.query.filter(SysUser.status == 'active') + # 查询 buyer_name 和 buyer_email,并去重 + query = db.session.query(StockBuy.buyer_name, StockBuy.buyer_email) \ + .filter(StockBuy.buyer_name.isnot(None), StockBuy.buyer_name != '') + if keyword: kw = f'%{keyword}%' - query = query.filter(db.or_( - SysUser.username.ilike(kw), - SysUser.email.ilike(kw) + query = query.filter(or_( + StockBuy.buyer_name.ilike(kw), + StockBuy.buyer_email.ilike(kw) )) - query = query.order_by(SysUser.username) + + # 按名字去重,取最新的记录(这里简单做 distinct,具体业务如果一个人有多个邮箱可能需要更复杂逻辑,这里简化为 distinct 组合) + results = query.distinct().limit(20).all() + users = [] - for u in query.limit(20).all(): + for row in results: users.append({ - 'value': u.username, - 'email': u.email + 'value': row.buyer_name, # 前端 autocomplete 显示的值 + 'email': row.buyer_email or '' }) return users except Exception: + traceback.print_exc() return [] # ============================================================ - # 8. [新增] 链接建议 (根据 base_id) + # 8. 链接建议 (基于 base_id) # ============================================================ @staticmethod def get_history_links(base_id, link_type='original'): diff --git a/inventory-web/src/api/inbound/buy.ts b/inventory-web/src/api/inbound/buy.ts index 1501f58..845cdd5 100644 --- a/inventory-web/src/api/inbound/buy.ts +++ b/inventory-web/src/api/inbound/buy.ts @@ -80,7 +80,7 @@ export function getUserSuggestions(params: any) { }) } -// 10. [新增] 链接建议 +// 10. 链接建议 export function getLinkSuggestions(params: any) { return request({ url: '/inbound/buy/suggestions/links', diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index b187ffc..b1420f4 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -98,13 +98,6 @@ - - - - Preview Image { if (!form.base_id) { - // 如果没有选物料,不给建议,或者可以给空 cb([]) return } @@ -623,14 +612,13 @@ const handleSupplierSelect = (item: any) => { form.supplier_name = item.value } -// 2. 采购人建议 (全局搜索 + 系统用户) +// 2. 采购人建议 (全局,来自历史 buy 表) const fetchUserSuggestions = async (query: string, cb: any) => { try { const res: any = await getUserSuggestions({ keyword: query }) if (res.code === 200) { - // 假设后端返回 [{value: '张三', email: 'zhangsan@xxx.com'}, ...] - const users = res.data.map((user: any) => ({ value: user.value, email: user.email })) - cb(users) + // res.data = [{value: '张三', email: 'xx@xx.com'}] + cb(res.data) } else { cb([]) } @@ -641,7 +629,7 @@ const fetchUserSuggestions = async (query: string, cb: any) => { const querySearchPurchaser = (qs: string, cb: any) => fetchUserSuggestions(qs, cb) const handlePurchaserSelect = (item: any) => { form.purchaser = item.value - // 核心:选中采购人时,自动填入邮箱 + // 自动填充邮箱 if (item.email) { form.purchaser_email = item.email } @@ -653,7 +641,6 @@ const fetchLinkSuggestions = async (query: string, cb: any, type: 'original' | ' try { const res: any = await getLinkSuggestions({ base_id: form.base_id, type }) if (res.code === 200) { - // 后端返回 ['http://...', 'http://...'] const links = res.data.map((link: string) => ({ value: link })) const filtered = query ? links.filter((item:any) => item.value.toLowerCase().includes(query.toLowerCase())) : links cb(filtered) @@ -686,10 +673,6 @@ const onMaterialSelected = (val: number) => { form.category = item.category form.unit = item.unit form.material_type = item.type - // 切换物料后,清空跟物料相关的供应商、链接,因为它们不再适用新物料 - // form.supplier_name = '' // 可选:是否清空 - // form.source_link = '' - // form.detail_link = '' checkHistoryAndSetMode(item.id) } } @@ -699,7 +682,6 @@ const onMaterialSelected = (val: number) => { // ------------------------------------ const validateUnique = (rule: any, value: string, callback: any) => { if (!value) return callback() - // 前端仅做当前页面的简单重复提示,真正的校验在后端 const isDuplicate = tableData.value.some((row: any) => { if (dialogStatus.value === 'update' && row.id === form.id) return false if (rule.field === 'serial_number' && row.serial_number === value) return true @@ -724,12 +706,11 @@ const rules = { // 自动计算批号逻辑 const checkHistoryAndSetMode = async (baseId: number) => { try { - const res: any = await getBuyList({page: 1, pageSize: 1000}) // 获取最近数据 + const res: any = await getBuyList({page: 1, pageSize: 1000}) const historyItems = (res.data.items || []).filter((item: any) => item.base_id === baseId) if (historyItems.length > 0) { modeLocked.value = true - // 找最新的那条记录 const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0] if (latest.serial_number) { entryMode.value = 'serial' @@ -738,7 +719,6 @@ const checkHistoryAndSetMode = async (baseId: number) => { } else { entryMode.value = 'batch' form.serial_number = '' - // 自动递增批号 form.batch_number = incrementBatchNumber(latest.batch_number || '000000') } } else { @@ -807,7 +787,7 @@ const handleUpdate = (row: any) => { source_link: row.source_link, detail_link: row.detail_link, arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || [] }) - // 核心:回显图片时,使用 getImageUrl 补全路径 + // 回显图片,补全路径 arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) })) const reports = form.inspection_report || [] const reportImgs = reports.filter(r => !isExternalLink(r)) @@ -844,11 +824,9 @@ const submitForm = async () => { } } else { await updateBuyInbound(form.id!, payload); ElMessage.success('更新成功') } - await fetchData() visible.value = false } catch (e: any) { - // 重点:捕获后端唯一性校验错误 ElMessage.error(e.msg || '操作失败') } finally { submitting.value = false } } @@ -859,15 +837,16 @@ const submitForm = async () => { // 图片/文件处理 (核心修复) // ------------------------------------ -// 1. 路径补全:如果是http开头则直接用,否则拼接 apiBaseUrl +// 1. 路径补全 const getImageUrl = (url: string) => { if (!url) return '' if (url.startsWith('http') || url.startsWith('https') || url.startsWith('blob:')) { return url } - // 拼接 API 基础路径,例如 http://localhost:5000 + /static/files/xxx.jpg - // 注意处理斜杠,防止双斜杠 - const baseUrl = apiBaseUrl.endsWith('/') ? apiBaseUrl.slice(0, -1) : apiBaseUrl + // 获取 VITE_APP_BASE_API,如果未设置则默认为空 + const apiBase = import.meta.env.VITE_APP_BASE_API || '' + // 去除末尾斜杠 + const baseUrl = apiBase.endsWith('/') ? apiBase.slice(0, -1) : apiBase const path = url.startsWith('/') ? url : '/' + url return baseUrl + path } @@ -892,16 +871,14 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec if (res.code === 200) { const newUrl = res.data.url // 后端返回的相对路径 - // 1. 存入表单数据 + // 1. 存入表单数据 (相对路径) form[targetField].push(newUrl) - // 2. 核心修复:显式更新 fileList 以确保缩略图显示 - // 需要拼接完整路径用于展示 + // 2. 显式更新 fileList (完整路径,用于显示) const fullUrl = getImageUrl(newUrl) const fileObj = { name: file.name, url: fullUrl, status: 'success', uid: file.uid } if (targetField === 'arrival_photo') { - // 替换或追加 const idx = arrivalFileList.value.findIndex(f => f.uid === file.uid) if (idx > -1) arrivalFileList.value[idx] = fileObj else arrivalFileList.value.push(fileObj) @@ -925,9 +902,8 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => { try { - // 这里需要反向查找,因为 uploadFile.url 可能是带域名的完整路径,而 form 里存的是相对路径 - // 简单比对末尾文件名 const filename = uploadFile.url.split('/').pop() + // 尝试匹配完整路径或文件名 const urlToRemove = form[targetField].find(u => u.endsWith(filename)) || uploadFile.url form[targetField] = form[targetField].filter(u => u !== urlToRemove) @@ -966,7 +942,7 @@ const handleCameraConfirm = async (file: File) => { // 更新表单 form[field].push(newUrl) - // 更新文件列表 (使用 getImageUrl 补全显示) + // 更新文件列表 (完整路径) const fileObj = { name: file.name, url: getImageUrl(newUrl) } if (field === 'arrival_photo') { arrivalFileList.value.push(fileObj)