Files
KCGL/inventory-backend/app/api/v1/inbound/base.py

596 lines
24 KiB
Python
Raw Normal View History

# 文件路径: app/api/v1/inbound/base.py
2026-02-25 09:55:25 +08:00
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, audit_log
from app.models.base import MaterialBase, MaterialWarningSetting
import traceback
2026-02-25 09:55:25 +08:00
import datetime
import json
2026-02-06 10:16:37 +08:00
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 []
# 超级管理员返回所有字段权限
perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板 本次提交包含本会话所有修改的最终统一提交 ## 权限系统重构 - permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions - field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase) - decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit - deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity) ## 采购模块 - purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤 - purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1 - purchase/index.vue: canApprove严格操作权限 + upload重复修复 ## 导出/入 - base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化 - import_service.py + import_api.py: Excel批量导入(模板+预览+执行) - ImportDialog.vue: 三步骤导入弹窗 ## 异步邮件 - email_service.py: send_email_async (守护线程) - inventory_task.py: send_email→send_email_async ## 前端对齐 - product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2 - buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载 - BomManage.vue: 懒加载分组 + 导入按钮 - list.vue: 导入按钮 - Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮 - outbound/create.vue: 出库类型必选 - AppMain.vue: 移除transition白屏修复 - material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
2026-07-17 13:07:12 +08:00
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):
perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板 本次提交包含本会话所有修改的最终统一提交 ## 权限系统重构 - permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions - field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase) - decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit - deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity) ## 采购模块 - purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤 - purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1 - purchase/index.vue: canApprove严格操作权限 + upload重复修复 ## 导出/入 - base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化 - import_service.py + import_api.py: Excel批量导入(模板+预览+执行) - ImportDialog.vue: 三步骤导入弹窗 ## 异步邮件 - email_service.py: send_email_async (守护线程) - inventory_task.py: send_email→send_email_async ## 前端对齐 - product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2 - buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载 - BomManage.vue: 懒加载分组 + 导入按钮 - list.vue: 导入按钮 - Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮 - outbound/create.vue: 出库类型必选 - AppMain.vue: 移除transition白屏修复 - material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
2026-07-17 13:07:12 +08:00
"""严格 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:
2026-02-25 09:55:25 +08:00
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', ''),
2026-02-25 09:55:25 +08:00
'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()
perf: 系统级性能优化与并发安全修复 ## 并发安全修复 (4处) - scrap.py: 报废执行添加 SELECT FOR UPDATE 悲观锁,消除 TOCTOU 竞态 - stock.py (adjust_stock): 盘点调整添加 for_update=True 行锁 - outbound_service.py: 低库存预警 SMTP 调用移到 commit 之后,避免长事务 - trans_service.py: execute_dispatch 按 (source_table, id) 排序 items,消除死锁风险 ## N+1 查询优化 (2处) - inventory_task.py: _prefetch_inventory_map 单条 UNION ALL+GROUP BY 替代循环内逐条查询(N*4次→2次) - stock.py (export_stocktake): get_borrowed_qty 批量 GROUP BY 替代逐条 TransBorrow 查询(~18000次→1次) ## BOM 列表性能重构 - bom_service.py: get_bom_list 单条 GROUP BY+string_agg+分页,消除 N+1 循环查询 - bom_service.py: 新增 get_bom_summary (轻量 GROUP BY category+COUNT) - bom.py: 新增 /api/v1/bom/summary 路由,/list 支持 category 过滤 ## Odoo 基础信息懒加载 - base_service.py: 新增 get_odoo_summary (GROUP BY category+COUNT) - base.py: 新增 /api/v1/inbound/base/odoo-summary 路由 - buyOdoo.vue: 懒加载分组架构 (fetchOdooSummary + loadGroupItems) - material_base.ts: 新增 getOdooSummary API ## 前端 Bug 修复 - BomManage.vue: 懒加载分组 (fetchBomSummary + loadGroupItems + collapse) - BomManage.vue: 适配新 API 格式 (res.data.items 替代 res.data) - buyOdoo.vue: 移除 "点击展开加载" 文字 - Selection.vue + borrow/apply/index.vue: openBomSelect 适配新 API 格式
2026-07-15 17:37:57 +08:00
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
# ==============================================================================
2026-02-25 09:55:25 +08:00
# 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
2026-02-25 09:55:25 +08:00
# ==============================================================================
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
# ==============================================================================
@inbound_base_bp.route('/export', methods=['GET'])
@permission_required('material_list')
2026-02-25 09:55:25 +08:00
def export_data():
try:
# 获取筛选条件
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)
}
# 获取当前用户权限
user_permissions = get_current_user_permissions()
# 生成 Excel 文件流(传入用户权限进行脱敏)
file_stream = MaterialBaseService.export_excel(filters, user_permissions)
2026-02-25 09:55:25 +08:00
# 生成文件名:库存统计+年月日+时分秒 (北京时间 UTC+8)
bj_time = beijing_time()
filename = f"库存统计_{bj_time.strftime('%Y%m%d_%H%M%S')}.xlsx"
2026-02-25 09:55:25 +08:00
# 发送文件
# 注意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')
@audit_log(
module='基础信息管理',
action='新增',
get_target_name_fn=lambda: request.get_json().get('name') if request.get_json() else None
)
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')
@audit_log(
module='基础信息管理',
action='修改',
get_target_id_fn=lambda: request.view_args.get('id'),
get_target_name_fn=lambda: request.get_json().get('name') if request.get_json() else None
)
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')
@audit_log(
module='基础信息管理',
action='删除',
get_target_id_fn=lambda: request.view_args.get('id')
)
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
2026-04-29 15:40:43 +08:00
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,
2026-04-29 15:40:43 +08:00
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
# ==============================================================================
2026-04-29 15:40:43 +08:00
# 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:operation')
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