export_excel 的行级公司隔离原本嵌套在 `if filters:` 内部:
只要调用方不传或传空筛选条件,整段隔离会被跳过且不抛错 —— 静默失效。
(实测: 修复前 export_excel({}, None) 会导出跨公司全部 1816 行。)
现把 get_current_company_filter() 及其 filter_conditions.append 提到
if filters: 之前,无论有无筛选条件都绝对执行。
同时清理路由里 filters 字典的 'company' 死键 —— export_excel 从不消费它,
真正生效的是 get_current_company_filter() 直接读 request.args 上的公司标识。
注意:前端 handleExport 的 company 参数必须保留(已在代码中加注说明)。
实测 WAREHOUSE_MGR 带 ?company=IRIS 导出 1126 行仅 IRIS,不带则 1816 行
涵盖 IRIS+LICA —— 跨域角色按公司收窄范围完全依赖这个 query 参数。
实测(修复后):
SALES/IRIS filters={} → 1126 行, 仅 IRIS
SALES/IRIS filters={keyword:''}→ 1126 行, 仅 IRIS
WAREHOUSE_MGR ?company=IRIS → 1126 行, 仅 IRIS
WAREHOUSE_MGR (无参数) → 1816 行, IRIS+LICA
583 lines
24 KiB
Python
583 lines
24 KiB
Python
# 文件路径: app/api/v1/inbound/base.py
|
||
|
||
from flask import Blueprint, request, jsonify, send_file, g, current_app
|
||
from app.extensions import db, beijing_time
|
||
from app.services.inbound.base_service import MaterialBaseService
|
||
from app.utils.decorators import login_required, permission_required
|
||
from app.models.base import MaterialBase, MaterialWarningSetting
|
||
import traceback
|
||
import datetime
|
||
import json
|
||
|
||
inbound_base_bp = Blueprint('stock_base', __name__)
|
||
|
||
|
||
# ==============================================================================
|
||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||
# ==============================================================================
|
||
def get_current_user_permissions():
|
||
"""
|
||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||
此函数根据角色+公司查询数据库得到权限。
|
||
"""
|
||
from flask_jwt_extended import get_jwt
|
||
from app.services.auth_service import AuthService
|
||
claims = get_jwt()
|
||
user_role = claims.get('role')
|
||
user_company = claims.get('company_name', '')
|
||
if not user_role:
|
||
return []
|
||
# 超级管理员返回所有字段权限
|
||
from app.utils.constants import UserRole
|
||
if str(user_role).strip().upper() == UserRole.SUPER_ADMIN:
|
||
return [
|
||
'material_list:*',
|
||
'material_list:id',
|
||
'material_list:companyName',
|
||
'material_list:name',
|
||
'material_list:commonName',
|
||
'material_list:category',
|
||
'material_list:type',
|
||
'material_list:spec',
|
||
'material_list:unit',
|
||
'material_list:inventoryCount',
|
||
'material_list:availableCount',
|
||
'material_list:files',
|
||
'material_list:isEnabled',
|
||
'material_list:referencePrice',
|
||
'material_list:operation'
|
||
]
|
||
perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
|
||
# 合并菜单和元素权限
|
||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||
return perms
|
||
|
||
|
||
def _invalidate_specs_cache():
|
||
"""规格连号缓存失效(新增/修改/删除基础信息时调用)"""
|
||
try:
|
||
from app.extensions import redis_client
|
||
if redis_client:
|
||
redis_client.delete('inventory:specs:grouped')
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def filter_item_by_permissions(item_dict, user_permissions):
|
||
"""严格 Default Deny 字段过滤 (see app/utils/field_permissions.py)"""
|
||
from app.utils.field_permissions import apply_strict_rbac
|
||
return apply_strict_rbac(item_dict, 'MaterialBase', user_permissions)
|
||
|
||
|
||
# ==============================================================================
|
||
# 1. 搜索接口 (GET /api/v1/inbound/base/search)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/search', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def search_base():
|
||
try:
|
||
keyword = request.args.get('keyword', '')
|
||
data = MaterialBaseService.search_material(keyword)
|
||
# 字段级脱敏
|
||
user_permissions = get_current_user_permissions()
|
||
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||
return jsonify({"code": 200, "msg": "success", "data": filtered_data})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 1.1 计量单位字典接口 (GET /api/v1/inbound/base/units)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/units', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def get_unit_dict():
|
||
"""
|
||
获取所有已存在的非空计量单位(去重 + 排序),用于前端
|
||
新增/编辑弹窗中"计量单位"下拉框的历史记录。
|
||
"""
|
||
try:
|
||
units = MaterialBaseService.get_distinct_units()
|
||
return jsonify({"code": 200, "msg": "success", "data": units})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2. 列表接口 (GET /api/v1/inbound/base/list)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/list', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def get_list():
|
||
try:
|
||
page = request.args.get('pageNum', 1, type=int)
|
||
limit = request.args.get('pageSize', 10, type=int)
|
||
|
||
# 解析高级筛选条件
|
||
advanced_filters_raw = request.args.get('advancedFilters', '[]')
|
||
try:
|
||
advanced_filters_list = json.loads(advanced_filters_raw)
|
||
except:
|
||
advanced_filters_list = []
|
||
|
||
# 构造筛选条件
|
||
filters = {
|
||
'keyword': request.args.get('keyword', ''),
|
||
'company': request.args.get('company', ''),
|
||
'category': request.args.get('category', ''),
|
||
'type': request.args.get('type', ''),
|
||
'isEnabled': request.args.get('isEnabled', None),
|
||
'orderByColumn': request.args.get('orderByColumn', ''),
|
||
'isAsc': request.args.get('isAsc', None),
|
||
'advancedFilters': advanced_filters_list,
|
||
'enableWarningSort': request.args.get('enableWarningSort', 'false').lower() == 'true',
|
||
'has_stock': request.args.get('has_stock', ''),
|
||
'searchField': request.args.get('searchField', 'all')
|
||
}
|
||
|
||
user_permissions = get_current_user_permissions()
|
||
|
||
# 自动拦截:如果用户有预警查看权限,且当前没有按特定列手动排序,则强制开启预警智能排序
|
||
has_warning_perm = 'material_list:view_warning' in user_permissions
|
||
if has_warning_perm and not filters.get('orderByColumn'):
|
||
filters['enableWarningSort'] = True
|
||
|
||
result = MaterialBaseService.get_list(page, limit, filters, user_permissions)
|
||
# 字段级脱敏
|
||
user_permissions = get_current_user_permissions()
|
||
if result.get('items'):
|
||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 1.4 Odoo 分组摘要接口 (GET /api/v1/inbound/base/odoo-summary)
|
||
# 极轻量查询:仅 GROUP BY category + COUNT,不 JOIN 任何库存表
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/odoo-summary', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def get_odoo_summary():
|
||
try:
|
||
keyword = request.args.get('keyword', '').strip() or None
|
||
is_enabled_raw = request.args.get('isEnabled', None)
|
||
is_enabled = None
|
||
if is_enabled_raw is not None:
|
||
val = str(is_enabled_raw).lower()
|
||
if val in ('1', 'true', 'yes', 't'):
|
||
is_enabled = True
|
||
elif val in ('0', 'false', 'no', 'f'):
|
||
is_enabled = False
|
||
|
||
data = MaterialBaseService.get_odoo_summary(keyword=keyword, is_enabled=is_enabled)
|
||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/options', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def get_options():
|
||
try:
|
||
data = MaterialBaseService.get_distinct_options()
|
||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/export', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def export_data():
|
||
try:
|
||
# 获取筛选条件
|
||
# ★ 此处刻意不放 company:export_excel 从不消费该键,放进来是死参数、易误导。
|
||
# 真正生效的公司隔离由 export_excel 内的 get_current_company_filter() 直接读
|
||
# request.args 里的 company / company_name —— 所以前端仍需把它带在 query
|
||
# string 上,跨域角色(超管 / WAREHOUSE_MGR)按公司出报表就靠这个参数。
|
||
filters = {
|
||
'keyword': request.args.get('keyword', ''),
|
||
'category': request.args.get('category', ''),
|
||
'type': request.args.get('type', ''),
|
||
'isEnabled': request.args.get('isEnabled', None)
|
||
}
|
||
|
||
# 获取当前用户权限
|
||
user_permissions = get_current_user_permissions()
|
||
|
||
# 生成 Excel 文件流(传入用户权限进行脱敏)
|
||
file_stream = MaterialBaseService.export_excel(filters, user_permissions)
|
||
|
||
# 生成文件名:库存统计+年月日+时分秒 (北京时间 UTC+8)
|
||
bj_time = beijing_time()
|
||
filename = f"库存统计_{bj_time.strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||
|
||
# 发送文件
|
||
# 注意:download_name 仅在较新 Flask 版本有效,旧版本可能需要手动 header,
|
||
# 但通常浏览器下载名由前端 Blob 处理或 Content-Disposition 决定。
|
||
return send_file(
|
||
file_stream,
|
||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
as_attachment=True,
|
||
download_name=filename
|
||
)
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": f"导出失败: {str(e)}"}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/', methods=['POST'])
|
||
@permission_required('material_list:operation')
|
||
def create():
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||
|
||
# 获取当前用户权限
|
||
user_permissions = get_current_user_permissions()
|
||
# 字段到权限码的映射(与 filter_item_by_permissions 一致)
|
||
field_to_perm = {
|
||
'id': 'material_list:id',
|
||
'companyName': 'material_list:companyName',
|
||
'name': 'material_list:name',
|
||
'commonName': 'material_list:commonName',
|
||
'category': 'material_list:category',
|
||
'type': 'material_list:type',
|
||
'spec': 'material_list:spec',
|
||
'unit': 'material_list:unit',
|
||
'inventoryCount': 'material_list:inventoryCount',
|
||
'availableCount': 'material_list:availableCount',
|
||
'generalManual': 'material_list:files',
|
||
'generalImage': 'material_list:files',
|
||
'referencePrice': 'material_list:referencePrice',
|
||
'isEnabled': 'material_list:isEnabled'
|
||
}
|
||
# 过滤用户没有权限的字段
|
||
filtered_data = {}
|
||
# 如果拥有通配符权限,则不过滤
|
||
if 'material_list:*' in user_permissions:
|
||
filtered_data = data
|
||
else:
|
||
for key, value in data.items():
|
||
if key in field_to_perm:
|
||
perm_code = field_to_perm[key]
|
||
if perm_code in user_permissions:
|
||
filtered_data[key] = value
|
||
# 没有权限则跳过,不包含在 filtered_data 中
|
||
else:
|
||
# 不在映射中的字段,默认允许(例如 visibilityLevel)
|
||
filtered_data[key] = value
|
||
|
||
MaterialBaseService.create_material(filtered_data)
|
||
_invalidate_specs_cache()
|
||
return jsonify({"code": 200, "msg": "新增成功"})
|
||
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
|
||
|
||
|
||
# ==============================================================================
|
||
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
||
@permission_required('material_list:operation')
|
||
def update(id):
|
||
try:
|
||
data = request.get_json()
|
||
# 获取当前用户权限
|
||
user_permissions = get_current_user_permissions()
|
||
# 字段到权限码的映射(与 filter_item_by_permissions 一致)
|
||
field_to_perm = {
|
||
'id': 'material_list:id',
|
||
'companyName': 'material_list:companyName',
|
||
'name': 'material_list:name',
|
||
'commonName': 'material_list:commonName',
|
||
'category': 'material_list:category',
|
||
'type': 'material_list:type',
|
||
'spec': 'material_list:spec',
|
||
'unit': 'material_list:unit',
|
||
'inventoryCount': 'material_list:inventoryCount',
|
||
'availableCount': 'material_list:availableCount',
|
||
'generalManual': 'material_list:files',
|
||
'generalImage': 'material_list:files',
|
||
'referencePrice': 'material_list:referencePrice',
|
||
'isEnabled': 'material_list:isEnabled'
|
||
}
|
||
# 过滤用户没有权限的字段
|
||
filtered_data = {}
|
||
# 如果拥有通配符权限,则不过滤
|
||
if 'material_list:*' in user_permissions:
|
||
filtered_data = data
|
||
else:
|
||
for key, value in data.items():
|
||
if key in field_to_perm:
|
||
perm_code = field_to_perm[key]
|
||
if perm_code in user_permissions:
|
||
filtered_data[key] = value
|
||
# 没有权限则跳过,不包含在 filtered_data 中
|
||
else:
|
||
# 不在映射中的字段,默认允许(例如 visibilityLevel)
|
||
filtered_data[key] = value
|
||
# 使用过滤后的数据调用服务
|
||
MaterialBaseService.update_material(id, filtered_data)
|
||
_invalidate_specs_cache()
|
||
return jsonify({"code": 200, "msg": "修改成功"})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||
@permission_required('material_list:operation')
|
||
def delete(id):
|
||
try:
|
||
material_name = MaterialBaseService.delete_material(id)
|
||
_invalidate_specs_cache()
|
||
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.5 批量设置预警 API (POST /api/v1/inbound/base/warning/batch-set)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/warning/batch-set', methods=['POST'])
|
||
@permission_required('material_list:edit_warning')
|
||
def batch_set_warning():
|
||
"""
|
||
批量设置物料预警配置
|
||
请求体格式: [
|
||
{"baseId": 1, "isEnabled": true, "yellowThreshold": 10, "redThreshold": 5},
|
||
{"baseId": 2, "isEnabled": false}
|
||
]
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
if not isinstance(data, list):
|
||
return jsonify({"code": 400, "msg": "请求体必须为数组"})
|
||
|
||
updated_count = 0
|
||
created_count = 0
|
||
|
||
for item in data:
|
||
base_id = item.get('baseId')
|
||
if not base_id:
|
||
continue
|
||
|
||
# 查找物料是否存在
|
||
material = MaterialBase.query.get(base_id)
|
||
if not material:
|
||
current_app.logger.warning(f"物料ID {base_id} 不存在,跳过")
|
||
continue
|
||
|
||
# 查找现有预警设置
|
||
warning = MaterialWarningSetting.query.filter_by(base_id=base_id).first()
|
||
|
||
if warning:
|
||
# 更新现有记录
|
||
if 'isEnabled' in item:
|
||
warning.is_enabled = bool(item['isEnabled'])
|
||
# 安全转换阈值,None 默认转为 0
|
||
yellow_val = item.get('yellowThreshold')
|
||
red_val = item.get('redThreshold')
|
||
warning.yellow_threshold = float(yellow_val) if yellow_val is not None else 0
|
||
warning.red_threshold = float(red_val) if red_val is not None else 0
|
||
warning.yellow_emails = item.get('yellowEmails', warning.yellow_emails)
|
||
warning.red_emails = item.get('redEmails', warning.red_emails)
|
||
updated_count += 1
|
||
else:
|
||
# 创建新记录
|
||
yellow_val = item.get('yellowThreshold')
|
||
red_val = item.get('redThreshold')
|
||
warning = MaterialWarningSetting(
|
||
base_id=base_id,
|
||
is_enabled=item.get('isEnabled', False),
|
||
yellow_threshold=float(yellow_val) if yellow_val is not None else 0,
|
||
red_threshold=float(red_val) if red_val is not None else 0,
|
||
yellow_emails=item.get('yellowEmails', ''),
|
||
red_emails=item.get('redEmails', '')
|
||
)
|
||
db.session.add(warning)
|
||
created_count += 1
|
||
|
||
db.session.commit()
|
||
return jsonify({
|
||
"code": 200,
|
||
"msg": "批量设置成功",
|
||
"data": {
|
||
"created": created_count,
|
||
"updated": updated_count
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量设置预警失败: {str(e)}")
|
||
return jsonify({"code": 500, "msg": f"批量设置预警失败: {str(e)}"}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.6 标记已采购 API (POST /api/v1/inbound/base/warning/mark-ordered)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/warning/mark-ordered', methods=['POST'])
|
||
@permission_required('material_list:edit_warning')
|
||
def mark_warning_ordered():
|
||
"""
|
||
前端标记预警物料已处理采购(标记 is_ordered)
|
||
请求体格式: {"baseId": 123, "isOrdered": true}
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||
|
||
base_id = data.get('baseId')
|
||
if not base_id:
|
||
return jsonify({"code": 400, "msg": "baseId 不能为空"}), 400
|
||
|
||
is_ordered = bool(data.get('isOrdered', False))
|
||
|
||
warning = MaterialWarningSetting.query.filter_by(base_id=base_id).first()
|
||
if not warning:
|
||
return jsonify({"code": 404, "msg": f"物料ID {base_id} 的预警配置不存在"}), 404
|
||
|
||
warning.is_ordered = is_ordered
|
||
db.session.commit()
|
||
|
||
status_text = "已标记为已采购" if is_ordered else "已重置为未采购"
|
||
return jsonify({
|
||
"code": 200,
|
||
"msg": status_text,
|
||
"data": warning.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"标记已采购失败: {str(e)}")
|
||
return jsonify({"code": 500, "msg": f"标记已采购失败: {str(e)}"}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.7 批量设置强制质检 API (POST /api/v1/inbound/base/batch-inspection)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/batch-inspection', methods=['POST'])
|
||
@permission_required('material_list:operation')
|
||
def batch_set_inspection():
|
||
"""
|
||
批量设置物料强制质检标记
|
||
请求体格式: {
|
||
"ids": [1, 2, 3],
|
||
"isInspectionRequired": true
|
||
}
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||
|
||
ids = data.get('ids', [])
|
||
is_inspection_required = bool(data.get('isInspectionRequired', False))
|
||
|
||
if not ids:
|
||
return jsonify({"code": 400, "msg": "请选择要设置的物料"}), 400
|
||
|
||
updated_count = 0
|
||
for base_id in ids:
|
||
material = MaterialBase.query.get(base_id)
|
||
if material:
|
||
material.is_inspection_required = is_inspection_required
|
||
updated_count += 1
|
||
|
||
db.session.commit()
|
||
return jsonify({
|
||
"code": 200,
|
||
"msg": f"批量设置成功,已更新 {updated_count} 条记录",
|
||
"data": {
|
||
"updated": updated_count
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量设置强制质检失败: {str(e)}")
|
||
return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.8 批量设置“出库/借库需审批” (POST /api/v1/inbound/base/batch-approval)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/batch-approval', methods=['POST'])
|
||
@permission_required('material_list:isApprovalRequired')
|
||
def batch_set_approval_required():
|
||
"""
|
||
批量设置物料“出库/借库需审批”标记(仿强制质检批量接口)。
|
||
请求体: { "ids": [1,2,3], "isApprovalRequired": true }
|
||
"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||
|
||
ids = data.get('ids', [])
|
||
is_approval_required = bool(data.get('isApprovalRequired', False))
|
||
if not ids:
|
||
return jsonify({"code": 400, "msg": "请选择要设置的物料"}), 400
|
||
|
||
updated_count = 0
|
||
for base_id in ids:
|
||
material = MaterialBase.query.get(base_id)
|
||
if material:
|
||
material.is_approval_required = is_approval_required
|
||
updated_count += 1
|
||
db.session.commit()
|
||
return jsonify({
|
||
"code": 200,
|
||
"msg": f"批量设置成功,已更新 {updated_count} 条记录",
|
||
"data": {"updated": updated_count}
|
||
})
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量设置需审批失败: {str(e)}")
|
||
return jsonify({"code": 500, "msg": f"批量设置需审批失败: {str(e)}"}), 500
|
||
|
||
|
||
# ==============================================================================
|
||
# 2.7 智能分组求最大连号 API (GET /api/v1/inbound/base/spec-latest)
|
||
# ==============================================================================
|
||
@inbound_base_bp.route('/spec-latest', methods=['GET'])
|
||
@permission_required('material_list')
|
||
def get_spec_latest():
|
||
"""
|
||
获取所有规格型号的最大连号,按智能分组返回
|
||
返回格式: [{"group": "S", "latest": "S0115/S0115"}, {"group": "Opt4xxx", "latest": "Opt4018/Opt4018"}, ...]
|
||
"""
|
||
try:
|
||
data = MaterialBaseService.get_latest_specs()
|
||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return jsonify({"code": 500, "msg": str(e)}), 500
|